Project Goal
Build a command-line Todo app that persists tasks to a JSON file. Users can add, list, complete, delete, and filter tasks by status. This project covers file I/O, JSON serialization, CLI argument parsing, and OOP design.
Design
Task model class with fromJson/toJsonTodoRepository for CRUD and persistenceTodoCLI for command routing
Full Code
import 'dart:io';
import 'dart:convert';
class Task {
final int id;
String title;
bool completed;
final DateTime createdAt;
Task({required this.id, required this.title, this.completed = false, DateTime? createdAt})
: createdAt = createdAt ?? DateTime.now();
factory Task.fromJson(Map<String, dynamic> j) => Task(
id: j['id'],
title: j['title'],
completed: j['completed'] ?? false,
createdAt: DateTime.parse(j['created_at']),
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'completed': completed,
'created_at': createdAt.toIso8601String(),
};
@override
String toString() {
var status = completed ? '✓' : '○';
var date = '$${createdAt.month}/$${createdAt.day}';
return '[$status] #$id $title ($date)';
}
}
class TodoRepository {
final String _path;
List<Task> _tasks = [];
TodoRepository([this._path = 'todos.json']);
void load() {
var file = File(_path);
if (file.existsSync()) {
var data = jsonDecode(file.readAsStringSync()) as List;
_tasks = data.map((j) => Task.fromJson(j)).toList();
}
}
void _save() => File(_path).writeAsStringSync(
JsonEncoder.withIndent(' ').convert(_tasks.map((t) => t.toJson()).toList()));
Task add(String title) {
var id = _tasks.isEmpty ? 1 : _tasks.map((t) => t.id).reduce((a, b) => a > b ? a : b) + 1;
var task = Task(id: id, title: title);
_tasks.add(task);
_save();
return task;
}
List<Task> getAll({bool? completed}) =>
completed == null ? _tasks : _tasks.where((t) => t.completed == completed).toList();
bool complete(int id) {
var task = _tasks.cast<Task?>().firstWhere((t) => t?.id == id, orElse: () => null);
if (task == null) return false;
task.completed = true;
_save();
return true;
}
bool delete(int id) {
var before = _tasks.length;
_tasks.removeWhere((t) => t.id == id);
if (_tasks.length < before) { _save(); return true; }
return false;
}
void clearCompleted() {
_tasks.removeWhere((t) => t.completed);
_save();
}
}
void printHelp() {
print('''
Usage: todo <command> [args]
add <title> Add a new task
list List all tasks
list pending List pending tasks
list done List completed tasks
done <id> Mark task as completed
delete <id> Delete a task
clear Remove all completed tasks
help Show this help
''');
}
void main(List<String> args) {
var repo = TodoRepository()..load();
if (args.isEmpty) { printHelp(); return; }
switch (args[0]) {
case 'add':
if (args.length < 2) { print('Usage: todo add <title>'); return; }
var task = repo.add(args.skip(1).join(' '));
print('Added: $task');
case 'list':
bool? filter = args.length > 1 ? (args[1] == 'done') : null;
var tasks = repo.getAll(completed: filter);
if (tasks.isEmpty) { print('No tasks.'); return; }
tasks.forEach(print);
print('\nTotal: $${tasks.length}');
case 'done':
if (args.length < 2) { print('Usage: todo done <id>'); return; }
var ok = repo.complete(int.parse(args[1]));
print(ok ? 'Completed #$${args[1]}' : 'Task #$${args[1]} not found');
case 'delete':
if (args.length < 2) { print('Usage: todo delete <id>'); return; }
var ok = repo.delete(int.parse(args[1]));
print(ok ? 'Deleted #$${args[1]}' : 'Task #$${args[1]} not found');
case 'clear':
repo.clearCompleted();
print('Cleared completed tasks');
default:
printHelp();
}
}
Running
dart run todo.dart add "Learn Dart generics"
dart run todo.dart add "Build a Flutter app"
dart run todo.dart list
dart run todo.dart done 1
dart run todo.dart list pending
dart run todo.dart clear
Challenges
- Add priority levels (low, medium, high)
- Add due dates and overdue detection
- Add search by keyword
- Add categories/tags