第8章:实战项目:待办事项 App
我们将开发一个本地持久化的 Todo 应用,支持以下功能:
✅ 添加待办事项
✅ 标记完成状态
✅ 删除任务
✅ 本地保存(SharedPreferences)
一、项目结构设计
/lib
├── main.dart
├── models/todo.dart
├── pages/home_page.dart
├── providers/todo_provider.dart
└── widgets/todo_item.dart
二、模型层:Todo 数据结构
// models/todo.dart
class Todo {
String id;
String title;
bool isDone;
Todo({required this.id, required this.title, this.isDone = false});
factory Todo.fromJson(Map<String, dynamic> json) => Todo(
id: json['id'],
title: json['title'],
isDone: json['isDone'],
);
Map<String, dynamic> toJson() =>
{'id': id, 'title': title, 'isDone': isDone};
}
三、状态管理:TodoProvider
// providers/todo_provider.dart
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/todo.dart';
class TodoProvider with ChangeNotifier {
List<Todo> _todos = [];
List<Todo> get todos => _todos;
void addTodo(String title) {
_todos.add(Todo(id: DateTime.now().toString(), title: title));
saveTodos();
notifyListeners();
}
void toggleTodo(String id) {
final index = _todos.indexWhere((todo) => todo.id == id);
_todos[index].isDone = !_todos[index].isDone;
saveTodos();
notifyListeners();
}
void deleteTodo(String id) {
_todos.removeWhere((todo) => todo.id == id);
saveTodos();
notifyListeners();
}
Future<void> loadTodos() async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString('todos') ?? '[]';
final list = json.decode(data) as List;
_todos = list.map((e) => Todo.fromJson(e)).toList();
notifyListeners();
}
Future<void> saveTodos() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('todos', json.encode(_todos));
}
}
四、主程序入口
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'pages/home_page.dart';
import 'providers/todo_provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => TodoProvider()..loadTodos(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo App',
theme: ThemeData(primarySwatch: Colors.teal),
home: HomePage(),
);
}
}
五、首页 UI 与逻辑
// pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
import '../widgets/todo_item.dart';
class HomePage extends StatelessWidget {
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
final provider = Provider.of<TodoProvider>(context);
return Scaffold(
appBar: AppBar(title: Text('My Todos')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
decoration: InputDecoration(labelText: 'New task'),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
if (controller.text.trim().isNotEmpty) {
provider.addTodo(controller.text.trim());
controller.clear();
}
},
)
],
),
),
Expanded(
child: ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(todo: todo);
},
),
),
],
),
);
}
}
六、待办项组件
// widgets/todo_item.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../providers/todo_provider.dart';
class TodoItem extends StatelessWidget {
final Todo todo;
const TodoItem({required this.todo});
@override
Widget build(BuildContext context) {
final provider = Provider.of<TodoProvider>(context, listen: false);
return Dismissible(
key: Key(todo.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => provider.deleteTodo(todo.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone ? TextDecoration.lineThrough : null,
),
),
trailing: Checkbox(
value: todo.isDone,
onChanged: (_) => provider.toggleTodo(todo.id),
),
),
);
}
}
七、常见问题解析
❗ 问题 1:输入框内容清除失败
确保在 addTodo() 后调用了 controller.clear()。
❗ 问题 2:状态刷新失败
检查是否使用了 ChangeNotifierProvider,并在子组件中使用 Provider.of<T>(context) 或 Consumer<T>()。
❗ 问题 3:数据未持久化
请确认是否调用了 saveTodos(),以及 SharedPreferences 的 setString 是否 await 成功。