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), ), ); } }
Top comments (0)