
A to-do list is the classic intermediate project. It combines everything you’ve learned so far: collections to store tasks, file I/O to persist data, user input for interaction, and methods to organize code. When you’re done, you’ll have a working application you can actually use.
We’ll build this incrementally, starting with in-memory storage and adding file persistence. The final version saves tasks between sessions and handles edge cases gracefully.
What We’re Building
The to-do list application will:
- Add, complete, and delete tasks
- Display all tasks with their status
- Save tasks to a file automatically
- Load tasks when the program starts
- Handle invalid input without crashing
This project uses ArrayList, File I/O, Scanner, and object-oriented design.
Stage 1: The Task Class
Start by modeling a task. Each task has a description, completion status, and unique ID.
public class Task {
private int id;
private String description;
private boolean completed;
public Task(int id, String description) {
this.id = id;
this.description = description;
this.completed = false;
}
public Task(int id, String description, boolean completed) {
this.id = id;
this.description = description;
this.completed = completed;
}
public int getId() {
return id;
}
public String getDescription() {
return description;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
@Override
public String toString() {
String status = completed ? "[X]" : "[ ]";
return status + " " + id + ". " + description;
}
// Format for file storage: id|description|completed
public String toFileString() {
return id + "|" + description + "|" + completed;
}
// Parse from file format
public static Task fromFileString(String line) {
String[] parts = line.split("\\|");
int id = Integer.parseInt(parts[0]);
String description = parts[1];
boolean completed = Boolean.parseBoolean(parts[2]);
return new Task(id, description, completed);
}
}
The toFileString() and fromFileString() methods handle serialization. We use pipe characters as delimiters since they’re unlikely to appear in task descriptions.
Stage 2: TaskManager Class
The TaskManager handles all operations on tasks. It manages the list, generates IDs, and coordinates saving/loading.
import java.util.ArrayList;
import java.util.List;
public class TaskManager {
private List<Task> tasks;
private int nextId;
public TaskManager() {
this.tasks = new ArrayList<>();
this.nextId = 1;
}
public void addTask(String description) {
Task task = new Task(nextId++, description);
tasks.add(task);
System.out.println("Added: " + task.getDescription());
}
public boolean completeTask(int id) {
for (Task task : tasks) {
if (task.getId() == id) {
task.setCompleted(true);
System.out.println("Completed: " + task.getDescription());
return true;
}
}
System.out.println("Task not found with ID: " + id);
return false;
}
public boolean deleteTask(int id) {
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i).getId() == id) {
Task removed = tasks.remove(i);
System.out.println("Deleted: " + removed.getDescription());
return true;
}
}
System.out.println("Task not found with ID: " + id);
return false;
}
public void listTasks() {
if (tasks.isEmpty()) {
System.out.println("No tasks yet. Add one with 'add <description>'");
return;
}
System.out.println("\n--- Your Tasks ---");
for (Task task : tasks) {
System.out.println(task);
}
System.out.println("------------------\n");
}
public void listPending() {
System.out.println("\n--- Pending Tasks ---");
boolean found = false;
for (Task task : tasks) {
if (!task.isCompleted()) {
System.out.println(task);
found = true;
}
}
if (!found) {
System.out.println("All tasks completed!");
}
System.out.println("---------------------\n");
}
public int getTaskCount() {
return tasks.size();
}
public int getPendingCount() {
int count = 0;
for (Task task : tasks) {
if (!task.isCompleted()) {
count++;
}
}
return count;
}
public List<Task> getTasks() {
return tasks;
}
public void setNextId(int nextId) {
this.nextId = nextId;
}
}
Stage 3: File Storage
Now add persistence. The FileHandler class saves tasks to a text file and loads them on startup.
import java.io.*;
import java.util.List;
public class FileHandler {
private static final String FILE_NAME = "tasks.txt";
public static void saveTasks(TaskManager manager) {
try (PrintWriter writer = new PrintWriter(new FileWriter(FILE_NAME))) {
for (Task task : manager.getTasks()) {
writer.println(task.toFileString());
}
} catch (IOException e) {
System.out.println("Error saving tasks: " + e.getMessage());
}
}
public static void loadTasks(TaskManager manager) {
File file = new File(FILE_NAME);
if (!file.exists()) {
return; // No saved tasks yet
}
int maxId = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty()) {
Task task = Task.fromFileString(line);
manager.getTasks().add(task);
if (task.getId() > maxId) {
maxId = task.getId();
}
}
}
manager.setNextId(maxId + 1);
if (manager.getTaskCount() > 0) {
System.out.println("Loaded " + manager.getTaskCount() + " tasks from file.");
}
} catch (IOException e) {
System.out.println("Error loading tasks: " + e.getMessage());
} catch (Exception e) {
System.out.println("Error parsing tasks file: " + e.getMessage());
}
}
}
The file format stores one task per line: 1|Buy groceries|false. Simple and human-readable.
Stage 4: Main Application
The main class ties everything together with a command-line interface.
import java.util.Scanner;
public class TodoApp {
private static TaskManager manager = new TaskManager();
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
// Load existing tasks
FileHandler.loadTasks(manager);
printWelcome();
boolean running = true;
while (running) {
System.out.print("> ");
String input = scanner.nextLine().trim();
if (input.isEmpty()) {
continue;
}
running = processCommand(input);
}
scanner.close();
}
private static void printWelcome() {
System.out.println("\n=============================");
System.out.println(" Java To-Do List App");
System.out.println("=============================");
System.out.println("Commands:");
System.out.println(" add <task> - Add a new task");
System.out.println(" list - Show all tasks");
System.out.println(" pending - Show pending tasks");
System.out.println(" done <id> - Mark task as complete");
System.out.println(" delete <id> - Delete a task");
System.out.println(" clear - Delete all completed tasks");
System.out.println(" help - Show this menu");
System.out.println(" quit - Exit the app");
System.out.println("=============================\n");
}
private static boolean processCommand(String input) {
String[] parts = input.split("\\s+", 2);
String command = parts[0].toLowerCase();
switch (command) {
case "add":
if (parts.length < 2 || parts[1].trim().isEmpty()) {
System.out.println("Usage: add <task description>");
} else {
manager.addTask(parts[1].trim());
FileHandler.saveTasks(manager);
}
break;
case "list":
manager.listTasks();
break;
case "pending":
manager.listPending();
break;
case "done":
if (parts.length < 2) {
System.out.println("Usage: done <task id>");
} else {
try {
int id = Integer.parseInt(parts[1].trim());
manager.completeTask(id);
FileHandler.saveTasks(manager);
} catch (NumberFormatException e) {
System.out.println("Invalid ID. Use a number.");
}
}
break;
case "delete":
if (parts.length < 2) {
System.out.println("Usage: delete <task id>");
} else {
try {
int id = Integer.parseInt(parts[1].trim());
manager.deleteTask(id);
FileHandler.saveTasks(manager);
} catch (NumberFormatException e) {
System.out.println("Invalid ID. Use a number.");
}
}
break;
case "clear":
clearCompleted();
break;
case "help":
printWelcome();
break;
case "quit":
case "exit":
System.out.println("Goodbye! Your tasks are saved.");
return false;
default:
System.out.println("Unknown command. Type 'help' for options.");
}
return true;
}
private static void clearCompleted() {
int removed = 0;
for (int i = manager.getTasks().size() - 1; i >= 0; i--) {
if (manager.getTasks().get(i).isCompleted()) {
manager.getTasks().remove(i);
removed++;
}
}
if (removed > 0) {
FileHandler.saveTasks(manager);
System.out.println("Cleared " + removed + " completed task(s).");
} else {
System.out.println("No completed tasks to clear.");
}
}
}
Running the Application
Compile all files and run:
javac Task.java TaskManager.java FileHandler.java TodoApp.java
java TodoApp
Sample session:
=============================
Java To-Do List App
=============================
Commands:
add <task> - Add a new task
list - Show all tasks
pending - Show pending tasks
done <id> - Mark task as complete
delete <id> - Delete a task
clear - Delete all completed tasks
help - Show this menu
quit - Exit the app
=============================
> add Buy groceries
Added: Buy groceries
> add Finish Java project
Added: Finish Java project
> add Call mom
Added: Call mom
> list
--- Your Tasks ---
[ ] 1. Buy groceries
[ ] 2. Finish Java project
[ ] 3. Call mom
------------------
> done 1
Completed: Buy groceries
> done 3
Completed: Call mom
> list
--- Your Tasks ---
[X] 1. Buy groceries
[ ] 2. Finish Java project
[X] 3. Call mom
------------------
> pending
--- Pending Tasks ---
[ ] 2. Finish Java project
---------------------
> clear
Cleared 2 completed task(s).
> list
--- Your Tasks ---
[ ] 2. Finish Java project
------------------
> quit
Goodbye! Your tasks are saved.
When you run the app again, your tasks load automatically:
Loaded 1 tasks from file.
=============================
Java To-Do List App
...
The Complete Project Structure
todo-app/
Task.java - Task model class
TaskManager.java - Business logic
FileHandler.java - File persistence
TodoApp.java - Main application
tasks.txt - Data file (created automatically)
Enhancements to Try
The basic app works, but there’s room to grow. Here are some features to add:
Due Dates
Add a due date field to Task:
import java.time.LocalDate;
public class Task {
private int id;
private String description;
private boolean completed;
private LocalDate dueDate; // Can be null
// Add methods for setting/getting due date
// Update toFileString() and fromFileString() to include date
}
Priority Levels
public enum Priority {
LOW, MEDIUM, HIGH
}
public class Task {
private Priority priority = Priority.MEDIUM;
// Add sorting by priority in TaskManager
}
Categories or Tags
private List<String> tags = new ArrayList<>();
// Commands like: add Buy milk #groceries #urgent
// List by tag: list #groceries
Search Functionality
public void searchTasks(String keyword) {
System.out.println("\n--- Search Results ---");
for (Task task : tasks) {
if (task.getDescription().toLowerCase().contains(keyword.toLowerCase())) {
System.out.println(task);
}
}
System.out.println("----------------------\n");
}
Statistics
public void showStats() {
int total = tasks.size();
int completed = total - getPendingCount();
double percentage = total > 0 ? (completed * 100.0 / total) : 0;
System.out.println("\n--- Statistics ---");
System.out.println("Total tasks: " + total);
System.out.println("Completed: " + completed);
System.out.println("Pending: " + getPendingCount());
System.out.printf("Progress: %.1f%%\n", percentage);
System.out.println("------------------\n");
}
What You Practiced
This project reinforced several intermediate concepts:
Object-oriented design: Separating concerns into Task, TaskManager, FileHandler, and TodoApp classes. Each class has a single responsibility.
Collections: Using ArrayList to store and manipulate tasks. Iterating, searching, and removing elements.
File I/O: Reading and writing text files. Handling the case where the file doesn’t exist yet.
String manipulation: Parsing commands, splitting strings, formatting output.
Error handling: Catching NumberFormatException for invalid IDs, IOException for file operations.
State management: Keeping the task list and file in sync. Auto-saving after changes.
Next Steps
Once you’re comfortable with the console version, consider:
GUI version: Rebuild with JavaFX for a graphical interface with checkboxes and buttons.
Database storage: Replace the text file with SQLite using JDBC.
Web version: Create a REST API with Spring Boot and a web frontend.
Prerequisites: ArrayList in Java | Reading and Writing Files | Getting User Input with Scanner
Related: Build a Console Calculator | Java Classes and Objects | Java Collections Framework


