This content originally appeared on DEV Community and was authored by Ge Ji
today we’ll be building a to-do list app to put our Flutter core knowledge into practice. As a classic introductory project, a to-do list covers key concepts like page navigation, data display, and basic interactions, making it perfect for reinforcing what we’ve learned so far.
I. Requirements Analysis and Page Design
1. Core Requirements Breakdown
The core goal of our to-do list app is to help users manage daily tasks. The key features we need to implement include:
- Displaying all to-do tasks (categorized by completed/uncompleted status)
- Viewing detailed information of individual tasks
- Adding new tasks
- Editing existing tasks
- Marking task completion status
- Deleting tasks
- Basic settings (such as theme switch, about page, etc.)
2. Page Architecture Design
Based on the above requirements, we’ll design three core pages:
- Home Page (Task List Page): As the app’s entry point, it will display a title and an add button at the top, with a list in the middle showing all tasks. It supports pull-to-refresh and swipe-to-delete operations. List items will show task titles, creation times, and completion status markers.
- Detail Page: Accessed by clicking list items, it will display complete task information (title, content, creation time, deadline, etc.) with edit and delete buttons at the bottom.
- Settings Page: Accessed via the settings icon on the home page, it will contain switch components (like notification toggle), an “About Us” entry, and a clear cache button.
3. Prototype Sketch Concept
The home page will use the classic “AppBar+ListView” structure, with a “+” icon button on the right side of the AppBar for adding tasks. The detail page will use a ScrollView to avoid content overflow, with an AppBar at the top providing a back button. The settings page will use ListView.builder to construct a list of settings items, where each item consists of an icon, text, and an operation component (switch/arrow).
II. Project Architecture Setup: Directory Structure
A reasonable directory structure makes the project more maintainable. We’ll use the following organization:
1. pages Directory
Stores all complete pages, with each page in its own folder containing the corresponding Dart file. For example:
- home_page: Code related to the home page
- detail_page: Code related to the detail page
- setting_page: Code related to the settings page
2. widgets Directory
Stores reusable custom components, with subdirectories categorized by function:
- common: General components (like custom buttons, input fields)
- task: Task-related components (like task list items, task status labels)
3. utils Directory
Stores utility classes and helper methods:
- router.dart: Routing management tool
- date_utils.dart: Date processing tool
- storage_utils.dart: Local storage tool (to be implemented in detail in the next lesson)
4. models Directory
Stores data model classes, using Dart classes to define data structures:
// models/task_model.dart
class Task {
final String id; // Unique task identifier
String title; // Task title
String content; // Task content
DateTime createTime; // Creation time
DateTime? deadline; // Deadline (optional)
bool isCompleted; // Completion status
Task({
required this.id,
required this.title,
this.content = '',
DateTime? createTime,
this.deadline,
this.isCompleted = false,
}) : createTime = createTime ?? DateTime.now();
}
III. Implementation of Core Utility Classes
1. Date Processing Utility (utils/date_utils.dart)
import 'package:intl/intl.dart';
class DateUtils {
/// Formats date to "yyyy-MM-dd"
static String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
/// Formats date to "yyyy-MM-dd HH:mm"
static String formatDateTime(DateTime date) {
return DateFormat('yyyy-MM-dd HH:mm').format(date);
}
/// Formats date to friendly display (e.g., "Today 14:30", "Yesterday 10:15")
static String formatFriendly(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final dateOnly = DateTime(date.year, date.month, date.day);
if (dateOnly == today) {
return 'Today ${DateFormat('HH:mm').format(date)}';
} else if (dateOnly == yesterday) {
return 'Yesterday ${DateFormat('HH:mm').format(date)}';
} else if (date.year == now.year) {
return DateFormat('MM-dd HH:mm').format(date);
} else {
return DateFormat('yyyy-MM-dd HH:mm').format(date);
}
}
}
Before using, add the dependency to pubspec.yaml:
dependencies:
intl: ^0.20.2
2. Routing Management Utility (utils/router.dart)
import 'package:flutter/material.dart';
import '../pages/home_page.dart';
import '../pages/detail_page.dart';
import '../pages/setting_page.dart';
class Router {
// Route name constants
static const String home = '/home';
static const String detail = '/detail';
static const String setting = '/setting';
// Route configuration
static Map<String, WidgetBuilder> routes = {
home: (context) => const HomePage(),
setting: (context) => const SettingPage(),
// Detail page needs dynamic parameters, handled during navigation
};
// Generate routes
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case detail:
final args = settings.arguments as Map<String, dynamic>?;
return MaterialPageRoute(
builder: (context) => DetailPage(
task: args?['task'],
onSave: args?['onSave'],
),
);
default:
return MaterialPageRoute(
builder: (context) => const Scaffold(
body: Center(child: Text('Page not found')),
),
);
}
}
}
3. Application Entry (main.dart)
import 'package:flutter/material.dart' hide Router;
import 'pages/home_page.dart';
import 'utils/router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do List',
// App theme configuration
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
// Configure global text styles
textTheme: const TextTheme(
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
// Disable debug mode banner
debugShowCheckedModeBanner: false,
// Initial route
initialRoute: Router.home,
// Route table
routes: Router.routes,
// Generate dynamic routes
onGenerateRoute: Router.generateRoute,
// Home page
home: const HomePage(),
);
}
}
IV. Implementation of Basic Pages
1. Home Page Implementation (pages/home_page.dart)
The home page is the core entry point of the app, where we’ll implement task list display, an add task button, and a settings entry.
import 'package:flutter/material.dart';
import '../widgets/task/task_item.dart';
import '../models/task_model.dart';
import 'detail_page.dart';
import 'setting_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Mock task data
final List<Task> _tasks = [
Task(
id: '1',
title: 'Learn Flutter',
content: 'Complete the practical project in Lesson 16',
deadline: DateTime.now().add(const Duration(days: 1)),
),
Task(
id: '2',
title: 'Buy groceries',
content: 'Milk, bread, fruits',
isCompleted: true,
),
];
// Navigate to detail page
void _navigateToDetail({Task? task}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
task: task,
onSave: (newTask) {
setState(() {
if (task == null) {
// Add new task
_tasks.add(newTask);
} else {
// Edit existing task
final index = _tasks.indexWhere((t) => t.id == task.id);
if (index != -1) {
_tasks[index] = newTask;
}
}
});
},
),
),
);
}
// Toggle task completion status
void _toggleTaskStatus(String id) {
setState(() {
final task = _tasks.firstWhere((t) => t.id == id);
task.isCompleted = !task.isCompleted;
});
}
// Delete task
void _deleteTask(String id) {
setState(() {
_tasks.removeWhere((t) => t.id == id);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('To-Do List'),
actions: [
// Settings button
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingPage()),
);
},
),
],
),
body: _tasks.isEmpty
? const Center(child: Text('No tasks yet, add one!'))
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return TaskItem(
task: task,
onTap: () => _navigateToDetail(task: task),
onStatusChanged: _toggleTaskStatus,
onDelete: _deleteTask,
);
},
),
// Add task button
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => _navigateToDetail(),
),
);
}
}
2. Detail Page Implementation (pages/detail_page.dart)
The detail page handles task viewing, editing, and deletion, requiring form input and data transmission.
import 'package:flutter/material.dart' hide DateUtils;
import '../models/task_model.dart';
import '../utils/date_utils.dart';
class DetailPage extends StatefulWidget {
final Task? task;
final Function(Task) onSave;
const DetailPage({
super.key,
this.task,
required this.onSave,
});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
late final TextEditingController _titleController;
late final TextEditingController _contentController;
DateTime? _deadline;
bool _isCompleted = false;
@override
void initState() {
super.initState();
// Initialize form data (edit mode)
if (widget.task != null) {
_titleController = TextEditingController(text: widget.task!.title);
_contentController = TextEditingController(text: widget.task!.content);
_deadline = widget.task!.deadline;
_isCompleted = widget.task!.isCompleted;
} else {
// Add mode
_titleController = TextEditingController();
_contentController = TextEditingController();
}
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
// Save task
void _saveTask() {
if (_titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a task title')),
);
return;
}
final task = Task(
id: widget.task?.id ?? DateTime.now().microsecondsSinceEpoch.toString(),
title: _titleController.text,
content: _contentController.text,
createTime: widget.task?.createTime ?? DateTime.now(),
deadline: _deadline,
isCompleted: _isCompleted,
);
widget.onSave(task);
Navigator.pop(context);
}
// Select deadline
Future<void> _selectDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _deadline ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => _deadline = picked);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.task == null ? 'Add Task' : 'Edit Task'),
actions: [
// Delete button (only shown in edit mode)
if (widget.task != null)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
Navigator.pop(context);
// You can add a delete confirmation dialog here
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
// Task title input
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Task Title',
border: OutlineInputBorder(),
),
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
// Task content input
TextField(
controller: _contentController,
decoration: const InputDecoration(
labelText: 'Task Content',
border: OutlineInputBorder(),
),
maxLines: 4,
),
const SizedBox(height: 16),
// Deadline selection
ListTile(
title: const Text('Deadline'),
subtitle: Text(_deadline != null
? DateUtils.formatDate(_deadline!)
: 'Not set'),
trailing: const Icon(Icons.calendar_today),
onTap: _selectDate,
),
// Completion status toggle
SwitchListTile(
title: const Text('Completion Status'),
value: _isCompleted,
onChanged: (value) => setState(() => _isCompleted = value),
),
const SizedBox(height: 20),
// Save button
ElevatedButton(
onPressed: _saveTask,
child: const Padding(
padding: EdgeInsets.all(12),
child: Text('Save Task'),
),
),
],
),
),
);
}
}
3. Settings Page Implementation (pages/setting_page.dart)
The settings page provides basic configuration options for the app, displayed as a list of settings items.
import 'package:flutter/material.dart';
class SettingPage extends StatelessWidget {
const SettingPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: ListView(
children: [
// Notification settings
SwitchListTile(
title: const Text('Enable Notification Reminders'),
value: true, // Simulated enabled state
onChanged: (value) {
// Actual logic will be implemented in the state management section
},
),
// Dark mode settings (to be implemented in next lesson)
ListTile(
title: const Text('Dark Mode'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Navigate to dark mode settings page
},
),
// About us
ListTile(
title: const Text('About Us'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
showAboutDialog(
context: context,
applicationName: 'To-Do List',
applicationVersion: '1.0.0',
children: [
const Padding(
padding: EdgeInsets.only(top: 16),
child: Text('A simple and efficient task management tool to help you better plan your life and work.'),
),
],
);
},
),
// Clear cache
ListTile(
title: const Text('Clear Cache'),
trailing: const Icon(Icons.delete),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cache cleared')),
);
},
),
],
),
);
}
}
V. Custom Component Implementation (widgets/task/task_item.dart)
Implementing the task list item component to improve code reusability:
import 'package:flutter/material.dart';
import '../../models/task_model.dart';
import '../../utils/date_utils.dart';
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback onTap;
final Function(String) onStatusChanged;
final Function(String) onDelete;
const TaskItem({
super.key,
required this.task,
required this.onTap,
required this.onStatusChanged,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
onTap: onTap,
leading: Checkbox(
value: task.isCompleted,
onChanged: (value) => onStatusChanged(task.id),
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted ? TextDecoration.lineThrough : null,
color: task.isCompleted ? Colors.grey : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (task.deadline != null)
Text(
'Deadline: ${DateUtils.formatDate(task.deadline!)}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
'Created: ${DateUtils.formatDate(task.createTime)}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
// Right arrow icon
trailing: const Icon(Icons.arrow_forward_ios, size: 18),
onLongPress: () => onDelete(task.id),
),
);
}
}
This content originally appeared on DEV Community and was authored by Ge Ji