待办事项 App 完整开发
嗨,朋友!我是长安。
在这个项目中,我们将从零开始开发一个功能完整、界面精美的待办事项 App。准备好了吗?让我们开始吧!
📦 第一步:创建项目
打开终端,创建新项目:
flutter create todo_app
cd todo_app
📁 第二步:规划目录结构
一个好的目录结构能让代码更易维护。在 lib 目录下创建以下结构:
lib/
├── main.dart # 入口文件
├── models/ # 数据模型
│ └── todo.dart
├── pages/ # 页面
│ ├── home_page.dart
│ └── add_todo_page.dart
├── widgets/ # 可复用组件
│ └── todo_item.dart
└── services/ # 服务(数据持久化)
└── storage_service.dart
🏗️ 第三步:定义数据模型
创建 lib/models/todo.dart:
class Todo {
final String id;
String title;
String? description;
bool isCompleted;
DateTime createdAt;
Todo({
required this.id,
required this.title,
this.description,
this.isCompleted = false,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
// 从 JSON 创建 Todo 对象(用于数据持久化)
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String?,
isCompleted: json['isCompleted'] as bool? ?? false,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
// 转换为 JSON(用于数据持久化)
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'isCompleted': isCompleted,
'createdAt': createdAt.toIso8601String(),
};
}
// 复制并修改
Todo copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}
}
💾 第四步:数据持久化服务
首先,添加依赖。编辑 pubspec.yaml:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.2
uuid: ^4.2.1
然后运行:
flutter pub get
创建 lib/services/storage_service.dart:
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/todo.dart';
class StorageService {
static const String _todosKey = 'todos';
static const String _themeKey = 'isDarkMode';
// 保存待办事项列表
static Future<void> saveTodos(List<Todo> todos) async {
final prefs = await SharedPreferences.getInstance();
final todosJson = todos.map((todo) => todo.toJson()).toList();
await prefs.setString(_todosKey, jsonEncode(todosJson));
}
// 加载待办事项列表
static Future<List<Todo>> loadTodos() async {
final prefs = await SharedPreferences.getInstance();
final todosString = prefs.getString(_todosKey);
if (todosString == null) return [];
final List<dynamic> todosJson = jsonDecode(todosString);
return todosJson
.map((json) => Todo.fromJson(json as Map<String, dynamic>))
.toList();
}
// 保存主题设置
static Future<void> saveTheme(bool isDarkMode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_themeKey, isDarkMode);
}
// 加载主题设置
static Future<bool> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_themeKey) ?? false;
}
}
🎨 第五步:创建待办事项卡片组件
创建 lib/widgets/todo_item.dart:
import 'package:flutter/material.dart';
import '../models/todo.dart';
class TodoItem extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
const TodoItem({
super.key,
required this.todo,
required this.onToggle,
required this.onDelete,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Dismissible(
key: Key(todo.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDelete(),
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Colors.red.shade400,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.delete, color: Colors.white),
),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onToggle,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 复选框
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 28,
height: 28,
decoration: BoxDecoration(
color: todo.isCompleted
? theme.colorScheme.primary
: Colors.transparent,
border: Border.all(
color: todo.isCompleted
? theme.colorScheme.primary
: Colors.grey,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
child: todo.isCompleted
? const Icon(Icons.check, size: 18, color: Colors.white)
: null,
),
const SizedBox(width: 16),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
todo.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
color: todo.isCompleted
? Colors.grey
: theme.textTheme.bodyLarge?.color,
),
),
if (todo.description != null &&
todo.description!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
todo.description!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 8),
Text(
_formatDate(todo.createdAt),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
),
// 删除按钮
IconButton(
onPressed: onDelete,
icon: Icon(
Icons.delete_outline,
color: Colors.grey[400],
),
),
],
),
),
),
),
);
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
}
📝 第六步:创建添加待办页面
创建 lib/pages/add_todo_page.dart:
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/todo.dart';
class AddTodoPage extends StatefulWidget {
const AddTodoPage({super.key});
State<AddTodoPage> createState() => _AddTodoPageState();
}
class _AddTodoPageState extends State<AddTodoPage> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _submit() {
if (_formKey.currentState!.validate()) {
final todo = Todo(
id: const Uuid().v4(),
title: _titleController.text.trim(),
description: _descriptionController.text.trim().isEmpty
? null
: _descriptionController.text.trim(),
);
Navigator.pop(context, todo);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('添加待办'),
actions: [
TextButton(
onPressed: _submit,
child: const Text('保存'),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 标题输入
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: '标题',
hintText: '请输入待办事项...',
prefixIcon: const Icon(Icons.title),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入标题';
}
return null;
},
),
const SizedBox(height: 20),
// 描述输入
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: '描述(可选)',
hintText: '添加更多细节...',
prefixIcon: const Icon(Icons.description),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
maxLines: 4,
),
const SizedBox(height: 30),
// 提交按钮
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'添加待办',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
);
}
}
🏠 第七步:创建首页
创建 lib/pages/home_page.dart:
import 'package:flutter/material.dart';
import '../models/todo.dart';
import '../services/storage_service.dart';
import '../widgets/todo_item.dart';
import 'add_todo_page.dart';
class HomePage extends StatefulWidget {
final bool isDarkMode;
final VoidCallback onThemeToggle;
const HomePage({
super.key,
required this.isDarkMode,
required this.onThemeToggle,
});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Todo> _todos = [];
bool _isLoading = true;
void initState() {
super.initState();
_loadTodos();
}
Future<void> _loadTodos() async {
final todos = await StorageService.loadTodos();
setState(() {
_todos = todos;
_isLoading = false;
});
}
Future<void> _saveTodos() async {
await StorageService.saveTodos(_todos);
}
void _addTodo(Todo todo) {
setState(() {
_todos.insert(0, todo);
});
_saveTodos();
}
void _toggleTodo(int index) {
setState(() {
_todos[index] = _todos[index].copyWith(
isCompleted: !_todos[index].isCompleted,
);
});
_saveTodos();
}
void _deleteTodo(int index) {
final deletedTodo = _todos[index];
setState(() {
_todos.removeAt(index);
});
_saveTodos();
// 显示撤销提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已删除 "${deletedTodo.title}"'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
setState(() {
_todos.insert(index, deletedTodo);
});
_saveTodos();
},
),
duration: const Duration(seconds: 3),
),
);
}
Future<void> _navigateToAddPage() async {
final result = await Navigator.push<Todo>(
context,
MaterialPageRoute(builder: (context) => const AddTodoPage()),
);
if (result != null) {
_addTodo(result);
}
}
Widget build(BuildContext context) {
final completedCount = _todos.where((t) => t.isCompleted).length;
final totalCount = _todos.length;
return Scaffold(
appBar: AppBar(
title: const Text('我的待办事项'),
centerTitle: true,
actions: [
IconButton(
onPressed: widget.onThemeToggle,
icon: Icon(
widget.isDarkMode ? Icons.light_mode : Icons.dark_mode,
),
tooltip: widget.isDarkMode ? '切换到浅色模式' : '切换到深色模式',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _todos.isEmpty
? _buildEmptyState()
: _buildTodoList(completedCount, totalCount),
floatingActionButton: FloatingActionButton.extended(
onPressed: _navigateToAddPage,
icon: const Icon(Icons.add),
label: const Text('添加待办'),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.checklist,
size: 100,
color: Colors.grey[300],
),
const SizedBox(height: 20),
Text(
'暂无待办事项',
style: TextStyle(
fontSize: 20,
color: Colors.grey[500],
),
),
const SizedBox(height: 10),
Text(
'点击下方按钮添加新的待办',
style: TextStyle(
fontSize: 14,
color: Colors.grey[400],
),
),
],
),
);
}
Widget _buildTodoList(int completedCount, int totalCount) {
return Column(
children: [
// 进度条
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'已完成 $completedCount / $totalCount',
style: const TextStyle(fontWeight: FontWeight.w500),
),
Text(
'${totalCount > 0 ? (completedCount / totalCount * 100).toInt() : 0}%',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: totalCount > 0 ? completedCount / totalCount : 0,
minHeight: 8,
backgroundColor: Colors.grey[300],
),
),
],
),
),
// 列表
Expanded(
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 80),
itemCount: _todos.length,
itemBuilder: (context, index) {
return TodoItem(
todo: _todos[index],
onToggle: () => _toggleTodo(index),
onDelete: () => _deleteTodo(index),
);
},
),
),
],
);
}
}
🎯 第八步:更新入口文件
更新 lib/main.dart:
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
import 'services/storage_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isDarkMode = false;
void initState() {
super.initState();
_loadTheme();
}
Future<void> _loadTheme() async {
final isDarkMode = await StorageService.loadTheme();
setState(() {
_isDarkMode = isDarkMode;
});
}
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
StorageService.saveTheme(_isDarkMode);
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true,
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
),
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: HomePage(
isDarkMode: _isDarkMode,
onThemeToggle: _toggleTheme,
),
);
}
}
🏃 第九步:运行项目
flutter run
恭喜!你的待办事项 App 完成了!🎉
✨ 项目功能总结
| 功能 | 实现方式 |
|---|---|
| 添加待办 | 表单 + Navigator.pop 返回数据 |
| 完成状态切换 | setState + copyWith |
| 删除待办 | Dismissible 滑动删除 + SnackBar 撤销 |
| 数据持久化 | shared_preferences + JSON 序列化 |
| 主题切换 | ThemeData + themeMode |
| 进度显示 | LinearProgressIndicator |
📝 项目结构回顾
lib/
├── main.dart # 入口文件,主题配置
├── models/
│ └── todo.dart # 数据模型
├── pages/
│ ├── home_page.dart # 首页
│ └── add_todo_page.dart # 添加页面
├── widgets/
│ └── todo_item.dart # 待办卡片组件
└── services/
└── storage_service.dart # 数据持久化服务
💪 扩展练习
现在,尝试自己添加以下功能:
练习1:编辑功能
添加点击待办事项进入编辑页面的功能。
练习2:分类筛选
添加"全部"、"未完成"、"已完成"的筛选标签。
练习3:搜索功能
在顶部添加搜索框,实现待办事项搜索。
练习4:排序功能
实现按创建时间、完成状态排序。
练习2 答案提示
在 home_page.dart 中添加筛选功能:
enum TodoFilter { all, active, completed }
class _HomePageState extends State<HomePage> {
TodoFilter _filter = TodoFilter.all;
List<Todo> get _filteredTodos {
switch (_filter) {
case TodoFilter.active:
return _todos.where((t) => !t.isCompleted).toList();
case TodoFilter.completed:
return _todos.where((t) => t.isCompleted).toList();
case TodoFilter.all:
default:
return _todos;
}
}
// 在 AppBar 下方添加筛选按钮
Widget _buildFilterChips() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FilterChip(
label: const Text('全部'),
selected: _filter == TodoFilter.all,
onSelected: (_) => setState(() => _filter = TodoFilter.all),
),
const SizedBox(width: 8),
FilterChip(
label: const Text('未完成'),
selected: _filter == TodoFilter.active,
onSelected: (_) => setState(() => _filter = TodoFilter.active),
),
const SizedBox(width: 8),
FilterChip(
label: const Text('已完成'),
selected: _filter == TodoFilter.completed,
onSelected: (_) => setState(() => _filter = TodoFilter.completed),
),
],
),
);
}
}
🎉 总结
恭喜你完成了这个项目!通过这个实战,你学会了:
- ✅ 项目结构规划
- ✅ 数据模型设计
- ✅ 状态管理实践
- ✅ 页面导航和传参
- ✅ 本地数据持久化
- ✅ 主题系统实现
- ✅ 组件化开发
现在你已经具备了开发一个完整 Flutter App 的能力!继续探索,构建更多有趣的应用吧!
📚 推荐继续学习
- 状态管理进阶:Provider、Riverpod、Bloc
- 网络请求:Dio、http 包
- 数据库:SQLite、Hive
- Firebase 集成:认证、云数据库、推送
- 发布应用:Android 和 iOS 上架流程
由 编程指南 提供
