Zum Inhalt springen
Java Anfänger 60 min

Projekt: Todo-App

Baue eine vollständige Todo-Anwendung in Java mit Kategorien, Prioritäten, Datei-Persistenz, Streams und modernem Java.

Aktualisiert:

Projekt: Todo-App

Im letzten Projekt dieses Kurses baust du eine Todo-Anwendung mit Kategorien, Prioritaeten und Datei-Speicherung. Du wirst Streams, Enums, Records, File I/O und alles andere kombinieren, was du in diesem Kurs gelernt hast.

Was wir bauen

Unsere Todo-App kann:

  • Aufgaben erstellen, bearbeiten und loeschen
  • Kategorien und Prioritaeten zuweisen
  • Aufgaben als erledigt markieren
  • Filtern und sortieren (nach Prioritaet, Datum, Kategorie)
  • Aufgaben in einer Datei speichern und laden
  • Statistiken anzeigen

Schritt 1: Enums fuer Prioritaet und Kategorie

public enum Prioritaet {
    NIEDRIG("Niedrig", 1),
    MITTEL("Mittel", 2),
    HOCH("Hoch", 3),
    KRITISCH("Kritisch", 4);

    private final String anzeigeName;
    private final int stufe;

    Prioritaet(String anzeigeName, int stufe) {
        this.anzeigeName = anzeigeName;
        this.stufe = stufe;
    }

    public String getAnzeigeName() { return anzeigeName; }
    public int getStufe() { return stufe; }

    public boolean istDringend() {
        return stufe >= 3;
    }
}
public enum Kategorie {
    ARBEIT("Arbeit"),
    PRIVAT("Privat"),
    EINKAUF("Einkauf"),
    GESUNDHEIT("Gesundheit"),
    LERNEN("Lernen"),
    SONSTIGES("Sonstiges");

    private final String anzeigeName;

    Kategorie(String anzeigeName) {
        this.anzeigeName = anzeigeName;
    }

    public String getAnzeigeName() { return anzeigeName; }
}

Schritt 2: Die Todo-Klasse

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Todo {
    private static int naechsteId = 1;
    private static final DateTimeFormatter DATUM_FORMAT =
        DateTimeFormatter.ofPattern("dd.MM.yyyy");

    private final int id;
    private String titel;
    private String beschreibung;
    private Prioritaet prioritaet;
    private Kategorie kategorie;
    private boolean erledigt;
    private final LocalDate erstelltAm;
    private LocalDate faelligAm;

    public Todo(String titel, Prioritaet prioritaet, Kategorie kategorie) {
        this(titel, "", prioritaet, kategorie, null);
    }

    public Todo(String titel, String beschreibung, Prioritaet prioritaet,
                Kategorie kategorie, LocalDate faelligAm) {
        if (titel == null || titel.isBlank()) {
            throw new IllegalArgumentException("Titel darf nicht leer sein!");
        }
        this.id = naechsteId++;
        this.titel = titel;
        this.beschreibung = beschreibung;
        this.prioritaet = prioritaet;
        this.kategorie = kategorie;
        this.erledigt = false;
        this.erstelltAm = LocalDate.now();
        this.faelligAm = faelligAm;
    }

    // Fuer das Laden aus Datei
    Todo(int id, String titel, String beschreibung, Prioritaet prioritaet,
         Kategorie kategorie, boolean erledigt, LocalDate erstelltAm, LocalDate faelligAm) {
        this.id = id;
        this.titel = titel;
        this.beschreibung = beschreibung;
        this.prioritaet = prioritaet;
        this.kategorie = kategorie;
        this.erledigt = erledigt;
        this.erstelltAm = erstelltAm;
        this.faelligAm = faelligAm;
        if (id >= naechsteId) {
            naechsteId = id + 1;
        }
    }

    // Getter
    public int getId() { return id; }
    public String getTitel() { return titel; }
    public String getBeschreibung() { return beschreibung; }
    public Prioritaet getPrioritaet() { return prioritaet; }
    public Kategorie getKategorie() { return kategorie; }
    public boolean isErledigt() { return erledigt; }
    public LocalDate getErstelltAm() { return erstelltAm; }
    public LocalDate getFaelligAm() { return faelligAm; }

    // Setter
    public void setTitel(String titel) { this.titel = titel; }
    public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
    public void setPrioritaet(Prioritaet prioritaet) { this.prioritaet = prioritaet; }
    public void setKategorie(Kategorie kategorie) { this.kategorie = kategorie; }
    public void setFaelligAm(LocalDate faelligAm) { this.faelligAm = faelligAm; }

    public void alsErledigtMarkieren() {
        this.erledigt = true;
    }

    public void alsOffenMarkieren() {
        this.erledigt = false;
    }

    public boolean istUeberfaellig() {
        return faelligAm != null && !erledigt && LocalDate.now().isAfter(faelligAm);
    }

    public String kurzDarstellung() {
        var status = erledigt ? "[X]" : "[ ]";
        var prio = prioritaet.istDringend() ? "!" : " ";
        var frist = faelligAm != null ? " (bis " + faelligAm.format(DATUM_FORMAT) + ")" : "";
        var ueberfaellig = istUeberfaellig() ? " UEBERFAELLIG" : "";

        return "%s %s #%d [%s|%s] %s%s%s".formatted(
            status, prio, id,
            prioritaet.getAnzeigeName(), kategorie.getAnzeigeName(),
            titel, frist, ueberfaellig);
    }

    // Fuer Dateispeicherung
    public String zuCsv() {
        return "%d;%s;%s;%s;%s;%b;%s;%s".formatted(
            id, titel, beschreibung, prioritaet.name(), kategorie.name(),
            erledigt, erstelltAm.toString(),
            faelligAm != null ? faelligAm.toString() : "");
    }

    @Override
    public String toString() {
        return kurzDarstellung();
    }
}

Schritt 3: Die TodoListe-Klasse

import java.util.*;
import java.util.stream.Collectors;

public class TodoListe {
    private final List<Todo> todos;

    public TodoListe() {
        this.todos = new ArrayList<>();
    }

    public void hinzufuegen(Todo todo) {
        todos.add(todo);
    }

    public boolean entfernen(int id) {
        return todos.removeIf(t -> t.getId() == id);
    }

    public Optional<Todo> finden(int id) {
        return todos.stream()
            .filter(t -> t.getId() == id)
            .findFirst();
    }

    public List<Todo> alle() {
        return List.copyOf(todos);
    }

    // Filter-Methoden mit Streams
    public List<Todo> nachPrioritaet(Prioritaet prio) {
        return todos.stream()
            .filter(t -> t.getPrioritaet() == prio)
            .toList();
    }

    public List<Todo> nachKategorie(Kategorie kat) {
        return todos.stream()
            .filter(t -> t.getKategorie() == kat)
            .toList();
    }

    public List<Todo> offene() {
        return todos.stream()
            .filter(t -> !t.isErledigt())
            .toList();
    }

    public List<Todo> erledigte() {
        return todos.stream()
            .filter(Todo::isErledigt)
            .toList();
    }

    public List<Todo> ueberfaellige() {
        return todos.stream()
            .filter(Todo::istUeberfaellig)
            .toList();
    }

    public List<Todo> dringende() {
        return todos.stream()
            .filter(t -> t.getPrioritaet().istDringend() && !t.isErledigt())
            .toList();
    }

    // Sortierung
    public List<Todo> sortiertNachPrioritaet() {
        return todos.stream()
            .sorted(Comparator.comparingInt(
                (Todo t) -> t.getPrioritaet().getStufe()).reversed())
            .toList();
    }

    public List<Todo> sortiertNachDatum() {
        return todos.stream()
            .sorted(Comparator.comparing(Todo::getErstelltAm).reversed())
            .toList();
    }

    // Statistiken
    public Map<String, Object> statistiken() {
        var gesamt = todos.size();
        var offen = todos.stream().filter(t -> !t.isErledigt()).count();
        var erledigt = todos.stream().filter(Todo::isErledigt).count();
        var ueberfaellig = todos.stream().filter(Todo::istUeberfaellig).count();

        var nachKategorie = todos.stream()
            .collect(Collectors.groupingBy(Todo::getKategorie, Collectors.counting()));

        var nachPrioritaet = todos.stream()
            .collect(Collectors.groupingBy(Todo::getPrioritaet, Collectors.counting()));

        return Map.of(
            "gesamt", gesamt,
            "offen", offen,
            "erledigt", erledigt,
            "ueberfaellig", ueberfaellig,
            "nachKategorie", nachKategorie,
            "nachPrioritaet", nachPrioritaet
        );
    }

    public int groesse() { return todos.size(); }

    public void ausDateiLaden(List<Todo> geladene) {
        todos.clear();
        todos.addAll(geladene);
    }
}

Schritt 4: Datei-Speicherung

import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class TodoSpeicher {
    private static final String DATEINAME = "todos.csv";
    private static final String HEADER = "id;titel;beschreibung;prioritaet;kategorie;erledigt;erstelltAm;faelligAm";

    public static void speichern(List<Todo> todos) {
        var zeilen = new ArrayList<String>();
        zeilen.add(HEADER);
        for (var todo : todos) {
            zeilen.add(todo.zuCsv());
        }

        try {
            Files.write(Path.of(DATEINAME), zeilen);
            System.out.println(todos.size() + " Aufgaben gespeichert.");
        } catch (IOException e) {
            System.out.println("Fehler beim Speichern: " + e.getMessage());
        }
    }

    public static List<Todo> laden() {
        var pfad = Path.of(DATEINAME);
        if (!Files.exists(pfad)) {
            return new ArrayList<>();
        }

        try {
            var zeilen = Files.readAllLines(pfad);
            var todos = new ArrayList<Todo>();

            for (int i = 1; i < zeilen.size(); i++) { // Header ueberspringen
                try {
                    todos.add(parseTodo(zeilen.get(i)));
                } catch (Exception e) {
                    System.out.println("Warnung: Zeile " + (i + 1) + " uebersprungen: " + e.getMessage());
                }
            }

            System.out.println(todos.size() + " Aufgaben geladen.");
            return todos;
        } catch (IOException e) {
            System.out.println("Fehler beim Laden: " + e.getMessage());
            return new ArrayList<>();
        }
    }

    private static Todo parseTodo(String zeile) {
        var teile = zeile.split(";", -1);
        if (teile.length < 8) {
            throw new IllegalArgumentException("Ungueltige Zeile");
        }

        var id = Integer.parseInt(teile[0]);
        var titel = teile[1];
        var beschreibung = teile[2];
        var prioritaet = Prioritaet.valueOf(teile[3]);
        var kategorie = Kategorie.valueOf(teile[4]);
        var erledigt = Boolean.parseBoolean(teile[5]);
        var erstelltAm = LocalDate.parse(teile[6]);
        var faelligAm = teile[7].isEmpty() ? null : LocalDate.parse(teile[7]);

        return new Todo(id, titel, beschreibung, prioritaet, kategorie,
                       erledigt, erstelltAm, faelligAm);
    }
}

Schritt 5: Die Konsolen-App

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;

public class TodoApp {
    private final TodoListe todoListe;
    private final Scanner scanner;

    public TodoApp() {
        this.todoListe = new TodoListe();
        this.scanner = new Scanner(System.in);
    }

    public void starten() {
        // Gespeicherte Todos laden
        var geladene = TodoSpeicher.laden();
        if (!geladene.isEmpty()) {
            todoListe.ausDateiLaden(geladene);
        }

        System.out.println("=== Todo-App ===");
        System.out.println("Tippe 'hilfe' fuer Befehle.\n");

        zeigeUeberfaelligeWarnung();

        while (true) {
            System.out.print("\nTodo> ");
            var eingabe = scanner.nextLine().strip();
            if (eingabe.isEmpty()) continue;

            try {
                if (!verarbeiteEingabe(eingabe)) {
                    TodoSpeicher.speichern(todoListe.alle());
                    break;
                }
            } catch (Exception e) {
                System.out.println("Fehler: " + e.getMessage());
            }
        }

        System.out.println("Auf Wiedersehen!");
        scanner.close();
    }

    private void zeigeUeberfaelligeWarnung() {
        var ueberfaellige = todoListe.ueberfaellige();
        if (!ueberfaellige.isEmpty()) {
            System.out.println("ACHTUNG: %d ueberfaellige Aufgabe(n):".formatted(ueberfaellige.size()));
            for (var todo : ueberfaellige) {
                System.out.println("  " + todo.kurzDarstellung());
            }
        }
    }

    private boolean verarbeiteEingabe(String eingabe) {
        var befehl = eingabe.toLowerCase().split("\\s+")[0];
        return switch (befehl) {
            case "hilfe", "help" -> { zeigeHilfe(); yield true; }
            case "neu", "add" -> { neueTodo(); yield true; }
            case "liste", "list" -> { zeigeListe(eingabe); yield true; }
            case "done" -> { markiereErledigt(eingabe); yield true; }
            case "undone" -> { markiereOffen(eingabe); yield true; }
            case "del", "delete" -> { loeschen(eingabe); yield true; }
            case "edit" -> { bearbeiten(eingabe); yield true; }
            case "stats" -> { zeigeStatistiken(); yield true; }
            case "speichern", "save" -> {
                TodoSpeicher.speichern(todoListe.alle());
                yield true;
            }
            case "exit", "quit" -> false;
            default -> {
                System.out.println("Unbekannter Befehl. Tippe 'hilfe'.");
                yield true;
            }
        };
    }

    private void neueTodo() {
        System.out.print("Titel: ");
        var titel = scanner.nextLine().strip();

        System.out.print("Beschreibung (optional): ");
        var beschreibung = scanner.nextLine().strip();

        System.out.println("Prioritaet (1=Niedrig, 2=Mittel, 3=Hoch, 4=Kritisch): ");
        var prioWahl = scanner.nextLine().strip();
        var prioritaet = switch (prioWahl) {
            case "1" -> Prioritaet.NIEDRIG;
            case "2" -> Prioritaet.MITTEL;
            case "3" -> Prioritaet.HOCH;
            case "4" -> Prioritaet.KRITISCH;
            default -> Prioritaet.MITTEL;
        };

        System.out.println("Kategorie:");
        var kategorien = Kategorie.values();
        for (int i = 0; i < kategorien.length; i++) {
            System.out.printf("  %d. %s%n", i + 1, kategorien[i].getAnzeigeName());
        }
        System.out.print("Wahl: ");
        var katWahl = scanner.nextLine().strip();
        var kategorie = Kategorie.SONSTIGES;
        try {
            var index = Integer.parseInt(katWahl) - 1;
            if (index >= 0 && index < kategorien.length) {
                kategorie = kategorien[index];
            }
        } catch (NumberFormatException ignored) {}

        System.out.print("Faellig am (TT.MM.JJJJ, leer = kein Datum): ");
        var datumStr = scanner.nextLine().strip();
        LocalDate faelligAm = null;
        if (!datumStr.isEmpty()) {
            try {
                faelligAm = LocalDate.parse(datumStr,
                    DateTimeFormatter.ofPattern("dd.MM.yyyy"));
            } catch (DateTimeParseException e) {
                System.out.println("Unguelltiges Datum, wird ignoriert.");
            }
        }

        var todo = new Todo(titel, beschreibung, prioritaet, kategorie, faelligAm);
        todoListe.hinzufuegen(todo);
        System.out.println("Aufgabe #%d erstellt!".formatted(todo.getId()));
    }

    private void zeigeListe(String eingabe) {
        var teile = eingabe.toLowerCase().split("\\s+");
        List<Todo> liste;
        var filterBeschreibung = "Alle";

        if (teile.length > 1) {
            liste = switch (teile[1]) {
                case "offen" -> { filterBeschreibung = "Offene"; yield todoListe.offene(); }
                case "erledigt" -> { filterBeschreibung = "Erledigte"; yield todoListe.erledigte(); }
                case "dringend" -> { filterBeschreibung = "Dringende"; yield todoListe.dringende(); }
                case "ueberfaellig" -> { filterBeschreibung = "Ueberfaellige"; yield todoListe.ueberfaellige(); }
                case "prio" -> { filterBeschreibung = "Nach Prioritaet"; yield todoListe.sortiertNachPrioritaet(); }
                default -> todoListe.alle();
            };
        } else {
            liste = todoListe.alle();
        }

        System.out.println("\n=== %s Aufgaben (%d) ===".formatted(filterBeschreibung, liste.size()));
        if (liste.isEmpty()) {
            System.out.println("  Keine Aufgaben gefunden.");
        } else {
            for (var todo : liste) {
                System.out.println("  " + todo.kurzDarstellung());
            }
        }
    }

    private void markiereErledigt(String eingabe) {
        var id = parseId(eingabe);
        todoListe.finden(id).ifPresentOrElse(
            todo -> {
                todo.alsErledigtMarkieren();
                System.out.println("Aufgabe #%d als erledigt markiert!".formatted(id));
            },
            () -> System.out.println("Aufgabe #%d nicht gefunden.".formatted(id))
        );
    }

    private void markiereOffen(String eingabe) {
        var id = parseId(eingabe);
        todoListe.finden(id).ifPresentOrElse(
            todo -> {
                todo.alsOffenMarkieren();
                System.out.println("Aufgabe #%d als offen markiert.".formatted(id));
            },
            () -> System.out.println("Aufgabe #%d nicht gefunden.".formatted(id))
        );
    }

    private void loeschen(String eingabe) {
        var id = parseId(eingabe);
        if (todoListe.entfernen(id)) {
            System.out.println("Aufgabe #%d geloescht.".formatted(id));
        } else {
            System.out.println("Aufgabe #%d nicht gefunden.".formatted(id));
        }
    }

    private void bearbeiten(String eingabe) {
        var id = parseId(eingabe);
        var optional = todoListe.finden(id);
        if (optional.isEmpty()) {
            System.out.println("Aufgabe #%d nicht gefunden.".formatted(id));
            return;
        }
        var todo = optional.get();
        System.out.println("Bearbeite: " + todo.kurzDarstellung());
        System.out.print("Neuer Titel (leer = beibehalten): ");
        var neuerTitel = scanner.nextLine().strip();
        if (!neuerTitel.isEmpty()) {
            todo.setTitel(neuerTitel);
        }
        System.out.println("Aufgabe #%d aktualisiert.".formatted(id));
    }

    @SuppressWarnings("unchecked")
    private void zeigeStatistiken() {
        var stats = todoListe.statistiken();
        System.out.println("\n=== Statistiken ===");
        System.out.printf("  Gesamt:       %d%n", stats.get("gesamt"));
        System.out.printf("  Offen:        %d%n", stats.get("offen"));
        System.out.printf("  Erledigt:     %d%n", stats.get("erledigt"));
        System.out.printf("  Ueberfaellig: %d%n", stats.get("ueberfaellig"));

        System.out.println("\n  Nach Kategorie:");
        var nachKat = (Map<Kategorie, Long>) stats.get("nachKategorie");
        nachKat.forEach((k, v) ->
            System.out.printf("    %-12s %d%n", k.getAnzeigeName(), v));

        System.out.println("\n  Nach Prioritaet:");
        var nachPrio = (Map<Prioritaet, Long>) stats.get("nachPrioritaet");
        nachPrio.forEach((k, v) ->
            System.out.printf("    %-12s %d%n", k.getAnzeigeName(), v));
    }

    private int parseId(String eingabe) {
        var teile = eingabe.split("\\s+");
        if (teile.length < 2) {
            throw new IllegalArgumentException("Bitte ID angeben: z.B. 'done 1'");
        }
        return Integer.parseInt(teile[1]);
    }

    private void zeigeHilfe() {
        System.out.println("""

            === Befehle ===
            neu                  Neue Aufgabe erstellen
            liste [filter]       Aufgaben anzeigen
              Filter: offen, erledigt, dringend, ueberfaellig, prio
            done <id>            Aufgabe als erledigt markieren
            undone <id>          Aufgabe als offen markieren
            del <id>             Aufgabe loeschen
            edit <id>            Aufgabe bearbeiten
            stats                Statistiken anzeigen
            speichern            Aufgaben speichern
            hilfe                Diese Hilfe
            exit                 Beenden (speichert automatisch)
            """);
    }

    public static void main(String[] args) {
        new TodoApp().starten();
    }
}

Angewendete Konzepte

KonzeptWo im Projekt
EnumsPrioritaet, Kategorie mit Feldern und Methoden
KlassenTodo, TodoListe, TodoApp
RecordsKoennte fuer unveraenderliche Daten verwendet werden
CollectionsArrayList, HashMap, List.copyOf()
StreamsFilter, Sortierung, Gruppierung, Statistiken
Optionalfinden() gibt Optional<Todo> zurueck
File I/OCSV-Speicherung mit Files.write() und Files.readAllLines()
Exception Handlingtry-catch fuer Dateizugriff und Parsing
Switch ExpressionBefehlsverarbeitung, Enum-Auswahl
KapselungPrivate Felder, kontrollierte Methoden
LocalDateDatumsverarbeitung fuer Faelligkeiten

Uebungen

Uebung 1: Suche

Fuege eine Volltextsuche hinzu: suche <text> durchsucht Titel und Beschreibung.

Uebung 2: Tags

Erweitere Todos um beliebige Tags (z.B. #wichtig, #projekt-x). Fuege Filter nach Tags hinzu.

Uebung 3: JSON statt CSV

Aendere die Speicherung von CSV auf JSON-Format (du kannst einen einfachen JSON-String manuell bauen oder die Gson-Bibliothek verwenden).

Uebung 4: Unteraufgaben

Erweitere Todos um Unteraufgaben (Sub-Todos). Ein Todo ist erst erledigt, wenn alle Unteraufgaben erledigt sind.

Zusammenfassung

  • Du hast eine vollstaendige Todo-Anwendung mit Persistenz gebaut
  • Enums mit Feldern modellieren Prioritaeten und Kategorien elegant
  • Streams ermoeglicht maechtige Filter-, Sortier- und Aggregationsoperationen
  • File I/O speichert und laedt Daten zwischen Programmstarts
  • Optional vermeidet NullPointerExceptions bei der Suche
  • Schichten-Trennung: Datenmodell, Logik, Speicherung und UI sind getrennt

Pro-Tipp: Du hast jetzt drei vollstaendige Java-Projekte gebaut — herzlichen Glueckwunsch! Der naechste Schritt waere, eines dieser Projekte mit einem Build-Tool (Maven oder Gradle) aufzusetzen, Unit Tests mit JUnit zu schreiben und vielleicht eine REST-API mit Spring Boot zu bauen. Java ist eine riesige Welt mit unendlich vielen Moeglichkeiten — und du hast jetzt das Fundament, um alles weitere zu lernen!

Zurück zum Java Kurs