待办事项App是Flutter最经典的入门实战项目。别小看它——一个完整的Todo App需要处理数据建模、CRUD操作、状态管理、数据持久化、搜索过滤、排序分组等几乎所有App开发核心问题。本课将用Provider+sqflite构建一个生产级待办App。
好的数据模型是App的基石。我们的Todo模型包含优先级、标签、截止日期等实用字段。
// lib/models/todo.dart
class Todo {
final int? id;
final String title;
final String? description;
final Priority priority;
final bool isDone;
final List<String> tags;
final DateTime? dueDate;
final DateTime createdAt;
final DateTime? completedAt;
const Todo({
this.id,
required this.title,
this.description,
this.priority = Priority.normal,
this.isDone = false,
this.tags = const [],
this.dueDate,
required this.createdAt,
this.completedAt,
});
// 从数据库Map创建
factory Todo.fromMap(Map<String, dynamic> map) => Todo(
id: map['id'] as int?,
title: map['title'] as String,
description: map['description'] as String?,
priority: Priority.values[map['priority'] as int],
isDone: (map['is_done'] as int) == 1,
tags: (map['tags'] as String).split(',').where((t) => t.isNotEmpty).toList(),
dueDate: map['due_date'] != null
? DateTime.fromMillisecondsSinceEpoch(map['due_date'] as int)
: null,
createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int),
completedAt: map['completed_at'] != null
? DateTime.fromMillisecondsSinceEpoch(map['completed_at'] as int)
: null,
);
// 转为数据库Map
Map<String, dynamic> toMap() => {
if (id != null) 'id': id,
'title': title,
'description': description,
'priority': priority.index,
'is_done': isDone ? 1 : 0,
'tags': tags.join(','),
'due_date': dueDate?.millisecondsSinceEpoch,
'created_at': createdAt.millisecondsSinceEpoch,
'completed_at': completedAt?.millisecondsSinceEpoch,
};
// copyWith模式 - 不可变更新
Todo copyWith({
int? id, String? title, String? description,
Priority? priority, bool? isDone, List<String>? tags,
DateTime? dueDate, DateTime? createdAt, DateTime? completedAt,
}) => Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
priority: priority ?? this.priority,
isDone: isDone ?? this.isDone,
tags: tags ?? this.tags,
dueDate: dueDate ?? this.dueDate,
createdAt: createdAt ?? this.createdAt,
completedAt: completedAt ?? this.completedAt,
);
}
enum Priority { low, normal, high, urgent }
extension PriorityUI on Priority {
String get label => ['低', '普通', '高', '紧急'][index];
Color get color => [
Colors.grey, Colors.blue, Colors.orange, Colors.red
][index];
IconData get icon => [
Icons.arrow_downward, Icons.remove, Icons.arrow_upward, Icons.priority_high
][index];
}
// lib/data/todo_database.dart
class TodoDatabase {
static final TodoDatabase _instance = TodoDatabase._internal();
factory TodoDatabase() => _instance;
TodoDatabase._internal();
Database? _db;
Future<Database> get database async {
if (_db != null && _db!.isOpen) return _db!;
_db = await _initDb();
return _db!;
}
Future<Database> _initDb() async {
final dbPath = await getDatabasesPath();
return openDatabase(
join(dbPath, 'todos.db'),
version: 2,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
priority INTEGER NOT NULL DEFAULT 1,
is_done INTEGER NOT NULL DEFAULT 0,
tags TEXT DEFAULT '',
due_date INTEGER,
created_at INTEGER NOT NULL,
completed_at INTEGER
)
''');
// 创建索引加速查询
await db.execute(
'CREATE INDEX idx_todos_done ON todos(is_done)'
);
await db.execute(
'CREATE INDEX idx_todos_priority ON todos(priority)'
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute(
'ALTER TABLE todos ADD COLUMN tags TEXT DEFAULT \'\''
);
}
},
);
}
// CRUD 操作
Future<int> insert(Todo todo) async {
final db = await database;
return db.insert('todos', todo.toMap());
}
Future<List<Todo>> getAll({String? filter, String? search}) async {
final db = await database;
var where = <String>[];
var args = <dynamic>[];
if (filter == 'active') {
where.add('is_done = 0');
} else if (filter == 'completed') {
where.add('is_done = 1');
}
if (search != null && search.isNotEmpty) {
where.add('(title LIKE ? OR description LIKE ?)');
args.addAll(['%$search%', '%$search%']);
}
final maps = await db.query(
'todos',
where: where.isEmpty ? null : where.join(' AND '),
whereArgs: args.isEmpty ? null : args,
orderBy: 'is_done ASC, priority DESC, created_at DESC',
);
return maps.map((m) => Todo.fromMap(m)).toList();
}
Future<int> update(Todo todo) async {
final db = await database;
return db.update(
'todos', todo.toMap(),
where: 'id = ?', whereArgs: [todo.id],
);
}
Future<int> delete(int id) async {
final db = await database;
return db.delete('todos', where: 'id = ?', whereArgs: [id]);
}
Future<int> deleteCompleted() async {
final db = await database;
return db.delete('todos', where: 'is_done = 1');
}
}
// lib/providers/todo_provider.dart
class TodoProvider extends ChangeNotifier {
final TodoDatabase _db = TodoDatabase();
List<Todo> _todos = [];
String _filter = 'all'; // all, active, completed
String _searchQuery = '';
String _sortBy = 'created'; // created, priority, dueDate
bool _isLoading = false;
String? _error;
// Getters
List<Todo> get todos => _todos;
String get filter => _filter;
String get searchQuery => _searchQuery;
bool get isLoading => _isLoading;
String? get error => _error;
// 统计信息
int get totalCount => _todos.length;
int get activeCount => _todos.where((t) => !t.isDone).length;
int get completedCount => _todos.where((t) => t.isDone).length;
// 加载数据
Future<void> loadTodos() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_todos = await _db.getAll(
filter: _filter == 'all' ? null : _filter,
search: _searchQuery.isEmpty ? null : _searchQuery,
);
_isLoading = false;
notifyListeners();
} catch (e) {
_error = e.toString();
_isLoading = false;
notifyListeners();
}
}
// 添加待办
Future<void> addTodo(Todo todo) async {
await _db.insert(todo);
await loadTodos();
}
// 切换完成状态
Future<void> toggleTodo(Todo todo) async {
final updated = todo.copyWith(
isDone: !todo.isDone,
completedAt: !todo.isDone ? DateTime.now() : null,
);
await _db.update(updated);
await loadTodos();
}
// 更新待办
Future<void> updateTodo(Todo todo) async {
await _db.update(todo);
await loadTodos();
}
// 删除待办
Future<void> deleteTodo(int id) async {
await _db.delete(id);
await loadTodos();
}
// 清除已完成
Future<void> clearCompleted() async {
await _db.deleteCompleted();
await loadTodos();
}
// 设置过滤条件
void setFilter(String filter) {
_filter = filter;
loadTodos();
}
// 设置搜索关键词
void setSearch(String query) {
_searchQuery = query;
loadTodos();
}
}
// lib/pages/home_page.dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('待办事项'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => showSearch(
context: context,
delegate: TodoSearchDelegate(),
),
),
PopupMenuButton<String>(
onSelected: (value) {
final provider = context.read<TodoProvider>();
if (value == 'clear') {
provider.clearCompleted();
} else {
provider.setFilter(value);
}
},
itemBuilder: (_) => [
const PopupMenuItem(value: 'all', child: Text('全部')),
const PopupMenuItem(value: 'active', child: Text('未完成')),
const PopupMenuItem(value: 'completed', child: Text('已完成')),
const PopupMenuDivider(),
const PopupMenuItem(value: 'clear', child: Text('清除已完成')),
],
),
],
),
body: Consumer<TodoProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.todos.isEmpty) {
return const Center(child: Text('没有待办事项'));
}
return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoCard(
todo: todo,
onToggle: () => provider.toggleTodo(todo),
onDelete: () => provider.deleteTodo(todo.id!),
onTap: () => _navigateToEdit(context, todo),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _navigateToAdd(context),
child: const Icon(Icons.add),
),
bottomNavigationBar: Consumer<TodoProvider>(
builder: (_, provider, __) => BottomAppBar(
child: Text(
'${provider.activeCount} 项待办 / 共 ${provider.totalCount} 项',
textAlign: TextAlign.center,
),
),
),
);
}
}
class TodoCard extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
final VoidCallback onTap;
const TodoCard({
required this.todo,
required this.onToggle,
required this.onDelete,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(todo.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => onDelete(),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: ListTile(
leading: Checkbox(
value: todo.isDone,
onChanged: (_) => onToggle(),
activeColor: todo.priority.color,
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isDone ? TextDecoration.lineThrough : null,
color: todo.isDone ? Colors.grey : null,
),
),
subtitle: _buildSubtitle(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(todo.priority.icon, color: todo.priority.color, size: 16),
if (todo.dueDate != null)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
_formatDate(todo.dueDate!),
style: TextStyle(
fontSize: 12,
color: todo.dueDate!.isBefore(DateTime.now())
? Colors.red : Colors.grey,
),
),
),
],
),
onTap: onTap,
),
),
);
}
}
ReorderableListView让用户自定义待办顺序flutter_local_notifications在截止日期前发送通知你已构建了一个完整的待办事项App!掌握了:
下一课预告:天气App——网络请求、JSON解析、动态UI、定位服务,让你的App连接真实世界!