Build a To-Do List App

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

Scroll to Top