Projekt: Todo-App
Baue eine vollständige Todo-Anwendung in Java mit Kategorien, Prioritäten, Datei-Persistenz, Streams und modernem Java.
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
| Konzept | Wo im Projekt |
|---|---|
| Enums | Prioritaet, Kategorie mit Feldern und Methoden |
| Klassen | Todo, TodoListe, TodoApp |
| Records | Koennte fuer unveraenderliche Daten verwendet werden |
| Collections | ArrayList, HashMap, List.copyOf() |
| Streams | Filter, Sortierung, Gruppierung, Statistiken |
| Optional | finden() gibt Optional<Todo> zurueck |
| File I/O | CSV-Speicherung mit Files.write() und Files.readAllLines() |
| Exception Handling | try-catch fuer Dateizugriff und Parsing |
| Switch Expression | Befehlsverarbeitung, Enum-Auswahl |
| Kapselung | Private Felder, kontrollierte Methoden |
| LocalDate | Datumsverarbeitung 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!