全局依赖与核心配置
工程的基石在于合理的依赖管理与目录划分。我们直接切入 pubspec.yaml,抛弃冗杂的无用依赖,只引入最核心的工业级库。
| 依赖库 | 版本 | 核心作用 |
|---|---|---|
| flutter_riverpod | ^2.4.3 | 提供可预测、安全的全局状态管理 |
| sqflite | ^2.3.0 | 本地SQLite数据库支持,用于持久化存储任务 |
| path_provider | ^2.1.1 | 获取设备本地文件系统的路径,配合数据库使用 |
| intl | ^0.18.1 | 提供日期格式化与国际化支持 |
| equatable | ^2.0.5 | 简化对象值比较,优化状态刷新性能 |
配置文件源码
YAML
name: flutter_todo_pro
description: A professional TODO application built with Flutter.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.3
sqflite: ^2.3.0
path_provider: ^2.1.1
intl: ^0.18.1
equatable: ^2.0.5
uuid: ^4.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
目录架构设计
为了保证项目在后期扩展时依然具有高可维护性,我们采用按功能模块与职责分离的结构。
| 目录路径 | 职责说明 |
|---|---|
| lib/core/ | 存放全局常量、主题配置、工具类、异常处理等通用基础代码 |
| lib/data/ | 负责数据层,包含模型类(Models)与本地数据库操作(Database) |
| lib/providers/ | 存放Riverpod的状态管理提供者,连接数据层与UI层 |
| lib/ui/ | 存放所有视觉元素,划分为screens(页面)与widgets(可复用组件) |
数据模型层开发
数据模型是整个应用的血液。我们需要一个强类型的对象来承载每一个任务节点的各项属性。使用 equatable 可以极大地避免由于内存地址不同而导致的无意义UI重绘。
TodoModel 源码实现
Dart
import 'package:equatable/equatable.dart';
class Todo extends Equatable {
final String id;
final String title;
final String description;
final bool isCompleted;
final DateTime createdAt;
final DateTime? dueDate;
const Todo({
required this.id,
required this.title,
this.description = '',
this.isCompleted = false,
required this.createdAt,
this.dueDate,
});
Todo copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
DateTime? createdAt,
DateTime? dueDate,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
dueDate: dueDate ?? this.dueDate,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'isCompleted': isCompleted ? 1 : 0,
'createdAt': createdAt.toIso8601String(),
'dueDate': dueDate?.toIso8601String(),
};
}
factory Todo.fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'] as String,
title: map['title'] as String,
description: map['description'] as String,
isCompleted: (map['isCompleted'] as int) == 1,
createdAt: DateTime.parse(map['createdAt'] as String),
dueDate: map['dueDate'] != null ? DateTime.parse(map['dueDate'] as String) : null,
);
}
@override
List<Object?> get props => [id, title, description, isCompleted, createdAt, dueDate];
}
1: 实体类不可变性
通过将属性声明为final,并配合copyWith方法,我们确保了状态的不可变性,这是现代响应式编程的基础。
2: 序列化与反序列化
toMap和fromMap是与SQLite数据库进行交互的关键桥梁。需要特别注意SQLite不支持原生的布尔值,这里巧妙地将其转换为0和1整型存储。
本地数据库引擎构建
应用的核心驱动力在于其数据持久化能力。sqflite 提供了底层直接控制权,使得我们可以在离线状态下实现毫秒级的读取和写入。我们采用单例模式来管控数据库连接,避免多线程并发下的连接池泄漏问题。
DatabaseHelper 源码实现
Dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import '../models/todo_model.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _database;
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static const String tableTodos = 'todos';
static const String columnId = 'id';
static const String columnTitle = 'title';
static const String columnDescription = 'description';
static const String columnIsCompleted = 'isCompleted';
static const String columnCreatedAt = 'createdAt';
static const String columnDueDate = 'dueDate';
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'todo_pro.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $tableTodos (
$columnId TEXT PRIMARY KEY,
$columnTitle TEXT NOT NULL,
$columnDescription TEXT NOT NULL,
$columnIsCompleted INTEGER NOT NULL,
$columnCreatedAt TEXT NOT NULL,
$columnDueDate TEXT
)
''');
}
Future<int> insertTodo(Todo todo) async {
final db = await database;
return await db.insert(
tableTodos,
todo.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Todo>> getAllTodos() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
tableTodos,
orderBy: '$columnCreatedAt DESC',
);
return List.generate(maps.length, (i) {
return Todo.fromMap(maps[i]);
});
}
Future<int> updateTodo(Todo todo) async {
final db = await database;
return await db.update(
tableTodos,
todo.toMap(),
where: '$columnId = ?',
whereArgs: [todo.id],
);
}
Future<int> deleteTodo(String id) async {
final db = await database;
return await db.delete(
tableTodos,
where: '$columnId = ?',
whereArgs: [id],
);
}
}
3: 单例模式的实现逻辑
通过私有构造函数 _internal() 配合 factory 关键字,内存中始终只保留一个数据库操作实例,极大降低了资源损耗。
4: 冲突处理机制
在 insertTodo 方法中使用了 ConflictAlgorithm.replace,当插入具有相同ID的任务时,新数据会自动覆盖旧数据,避免主键冲突导致的崩溃。
状态管理层枢纽
有了数据模型和底层数据库,接下来需要构建 UI 与数据之间的桥梁。Riverpod 彻底抛弃了依赖树上下文(BuildContext)的限制,让状态管理变得如同写纯 Dart 代码一样丝滑。我们将创建一个 StateNotifier 来管理 TODO 列表的异步流。
TodoNotifier 与 Provider 源码
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
import '../data/database/database_helper.dart';
import '../data/models/todo_model.dart';
final databaseProvider = Provider<DatabaseHelper>((ref) {
return DatabaseHelper();
});
final todoListProvider = StateNotifierProvider<TodoNotifier, AsyncValue<List<Todo>>>((ref) {
final dbHelper = ref.watch(databaseProvider);
return TodoNotifier(dbHelper);
});
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
final DatabaseHelper _dbHelper;
TodoNotifier(this._dbHelper) : super(const AsyncValue.loading()) {
loadTodos();
}
Future<void> loadTodos() async {
try {
state = const AsyncValue.loading();
final todos = await _dbHelper.getAllTodos();
state = AsyncValue.data(todos);
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}
Future<void> addTodo(String title, String description, DateTime? dueDate) async {
const uuid = Uuid();
final newTodo = Todo(
id: uuid.v4(),
title: title,
description: description,
createdAt: DateTime.now(),
dueDate: dueDate,
);
try {
await _dbHelper.insertTodo(newTodo);
if (state is AsyncData) {
final currentTodos = state.value!;
state = AsyncValue.data([newTodo, ...currentTodos]);
} else {
await loadTodos();
}
} catch (e) {
// 真实业务中此处可对接日志监控系统
print('Insert Error: $e');
}
}
Future<void> toggleCompleted(String id) async {
if (state is! AsyncData) return;
final currentTodos = state.value!;
final todoIndex = currentTodos.indexWhere((todo) => todo.id == id);
if (todoIndex != -1) {
final todo = currentTodos[todoIndex];
final updatedTodo = todo.copyWith(isCompleted: !todo.isCompleted);
try {
await _dbHelper.updateTodo(updatedTodo);
final newTodos = List<Todo>.from(currentTodos);
newTodos[todoIndex] = updatedTodo;
state = AsyncValue.data(newTodos);
} catch (e) {
print('Update Error: $e');
}
}
}
Future<void> deleteTodo(String id) async {
if (state is! AsyncData) return;
try {
await _dbHelper.deleteTodo(id);
final currentTodos = state.value!;
state = AsyncValue.data(currentTodos.where((todo) => todo.id != id).toList());
} catch (e) {
print('Delete Error: $e');
}
}
}
5: AsyncValue 的妙用
利用 Riverpod 提供的 AsyncValue,可以无缝地处理状态的加载中(Loading)、成功(Data)与失败(Error)三种情形,后续在UI层无需手动编写冗长的 try-catch 或 FutureBuilder。
6: 乐观UI更新策略
在执行删除或添加操作时,我们先操作数据库,随后直接修改内存中的 state 数据。这避免了每次操作都需要重新去查询整张表,将列表刷新时间控制在亚毫秒级别。
核心UI构建与全局主题
在深入复杂交互前,制定统一的视觉规范是极其必要的。为了代码清晰,我们将主题相关的内容剥离到独立的常量类中,并通过 main.dart 注入全局配置。
色彩与主题规范配置
Dart
// lib/core/constants/app_colors.dart
import 'package:flutter/material.dart';
class AppColors {
static const Color primary = Color(0xFF2563EB);
static const Color secondary = Color(0xFF3B82F6);
static const Color background = Color(0xFFF3F4F6);
static const Color surface = Colors.white;
static const Color textPrimary = Color(0xFF1F2937);
static const Color textSecondary = Color(0xFF6B7280);
static const Color success = Color(0xFF10B981);
static const Color danger = Color(0xFFEF4444);
}
// lib/core/theme/app_theme.dart
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
primaryColor: AppColors.primary,
scaffoldBackgroundColor: AppColors.background,
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.primary,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
cardTheme: CardTheme(
color: AppColors.surface,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: AppColors.textPrimary, fontSize: 16),
bodyMedium: TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
);
}
}
程序入口 main.dart
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'ui/screens/home_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// ProviderScope 必须包裹整个应用,以初始化 Riverpod 上下文
runApp(
const ProviderScope(
child: TodoApp(),
),
);
}
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Todo Pro',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
home: const HomeScreen(),
);
}
}
7: 强制初始化绑定
WidgetsFlutterBinding.ensureInitialized() 确保在应用构建前,Flutter的渲染引擎已经与底层的操作系统插件通道完成握手。这对于后续依赖路径查找(path_provider)这类底层调用至关重要。
首页展示层
首页是应用的核心骨架,负责展示列表与各种状态下的UI反馈。利用 Riverpod 的 ref.watch 机制,页面能如同被赋予生命一般,随着底层数据库数据的变动而自动重构。
HomeScreen 核心结构
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/todo_provider.dart';
import '../widgets/todo_item_tile.dart';
import 'add_todo_sheet.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todoState = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(
title: const Text('我的任务清单'),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () {
// 后续扩展:任务过滤逻辑
},
)
],
),
body: todoState.when(
data: (todos) {
if (todos.isEmpty) {
return const Center(
child: Text(
'暂无任务,快去添加一个吧',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoItemTile(todo: todo);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text('加载失败: $error', style: const TextStyle(color: Colors.red)),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoSheet(context),
child: const Icon(Icons.add),
),
);
}
void _showAddTodoSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const AddTodoSheet(),
);
}
}
8: ConsumerWidget 的优势
由于集成了 Riverpod,我们不再继承传统的 StatelessWidget,而是使用 ConsumerWidget。这允许我们在 build 方法中直接拿到 WidgetRef 对象进行状态监听,大幅削减了样板代码。任务项交互组件开发
在构建列表展示时,每一个具体的任务节点不仅承担着信息展示的责任,更是用户高频交互的核心区域。为了让应用拥有符合直觉的操作体验,我们引入 Dismissible 组件来实现滑动删除功能,并结合 Riverpod 实现精准的局部刷新。
TodoItemTile 源码实现
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../data/models/todo_model.dart';
import '../../providers/todo_provider.dart';
import '../../core/constants/app_colors.dart';
class TodoItemTile extends ConsumerWidget {
final Todo todo;
const TodoItemTile({super.key, required this.todo});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hasDueDate = todo.dueDate != null;
final isOverdue = hasDueDate &&
todo.dueDate!.isBefore(DateTime.now()) &&
!todo.isCompleted;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Dismissible(
key: Key(todo.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: AppColors.danger,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.delete_outline, color: Colors.white, size: 28),
),
onDismissed: (_) {
ref.read(todoListProvider.notifier).deleteTodo(todo.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('任务已删除'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
// 后续可扩展:将删除的数据暂存内存,点击撤销重新插入数据库
ref.read(todoListProvider.notifier).addTodo(
todo.title,
todo.description,
todo.dueDate,
);
},
),
),
);
},
child: Card(
elevation: todo.isCompleted ? 0 : 2,
color: todo.isCompleted ? AppColors.surface.withOpacity(0.6) : AppColors.surface,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Transform.scale(
scale: 1.2,
child: Checkbox(
value: todo.isCompleted,
activeColor: AppColors.success,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
onChanged: (_) {
ref.read(todoListProvider.notifier).toggleCompleted(todo.id);
},
),
),
title: Text(
todo.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: todo.isCompleted ? AppColors.textSecondary : AppColors.textPrimary,
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (todo.description.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
todo.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: AppColors.textSecondary),
),
],
if (hasDueDate) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.calendar_today,
size: 14,
color: isOverdue ? AppColors.danger : AppColors.primary,
),
const SizedBox(width: 4),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(todo.dueDate!),
style: TextStyle(
fontSize: 12,
color: isOverdue ? AppColors.danger : AppColors.primary,
fontWeight: isOverdue ? FontWeight.bold : FontWeight.normal,
),
),
],
),
]
],
),
),
),
),
);
}
}
9: 滑动删除的丝滑体验
使用 Dismissible 组件包裹完整的 ListTile,通过监听 onDismissed 回调直接触发状态层的删除逻辑。我们同时加入了 SnackBar 联动,为用户提供视觉确认与挽回操作的余地,这是留存用户的关键细节。
10: 状态跨层级精准调用
在组件的交互回调内部(如 onChanged 或 onDismissed),我们使用 ref.read 而不是 ref.watch 来调用方法。执行动作不需要监听状态的持续变化,只需获取 Notifier 实例发送指令即可,彻底切断了无效渲染的源头。
底部弹窗表单构建
新建任务的过程需要在一个封闭且专注的上下文中进行。原生的 BottomSheet 提供了极佳的空间利用率。因为表单涉及到输入框的焦点管理和本地日期的暂存,这里我们需要将其升级为 ConsumerStatefulWidget。
AddTodoSheet 源码实现
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../providers/todo_provider.dart';
import '../../core/constants/app_colors.dart';
class AddTodoSheet extends ConsumerStatefulWidget {
const AddTodoSheet({super.key});
@override
ConsumerState<AddTodoSheet> createState() => _AddTodoSheetState();
}
class _AddTodoSheetState extends ConsumerState<AddTodoSheet> {
final _titleController = TextEditingController();
final _descController = TextEditingController();
DateTime? _selectedDate;
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_titleController.dispose();
_descController.dispose();
super.dispose();
}
Future<void> _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
onPrimary: Colors.white,
onSurface: AppColors.textPrimary,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
_selectedDate = picked;
});
}
}
void _submit() {
if (_formKey.currentState!.validate()) {
ref.read(todoListProvider.notifier).addTodo(
_titleController.text.trim(),
_descController.text.trim(),
_selectedDate,
);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
// 获取键盘高度,实现自适应布局
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Container(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 24,
bottom: bottomInset + 24,
),
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'创建新任务',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
TextFormField(
controller: _titleController,
autofocus: true,
decoration: InputDecoration(
labelText: '任务标题',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.task_alt),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '标题不能为空';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _descController,
maxLines: 3,
decoration: InputDecoration(
labelText: '任务描述 (可选)',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
alignLabelWithHint: true,
),
),
const SizedBox(height: 16),
InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.calendar_month, color: AppColors.primary),
const SizedBox(width: 12),
Text(
_selectedDate == null
? '设置截止日期'
: DateFormat('yyyy年 MM月 dd日').format(_selectedDate!),
style: TextStyle(
fontSize: 16,
color: _selectedDate == null ? Colors.grey : AppColors.textPrimary,
),
),
],
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'保存任务',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
}
11: 动态键盘自适应布局原理
在 BottomSheet 中使用 Padding 包裹整个表单内容,并通过 MediaQuery.of(context).viewInsets.bottom 动态获取当前设备软键盘的物理高度。将其叠加到弹窗的底部内边距上,彻底防止了软键盘弹起时遮挡输入框的严重体验缺陷。
12: 底层资源释放的严谨性
TextEditingController 这种直接涉及底层内存分配与引擎通信的对象,必须在其宿主组件的 dispose 生命周期钩子中被显式调用销毁逻辑。这杜绝了应用长期驻留后台或频繁开闭表单时引发的内存泄漏隐患。
工具类矩阵与模块化解析
应用越庞大,越需要将具体的业务逻辑与通用的工具逻辑分离开来。日期处理就是一个典型的通用切面。
| 工具模块名称 | 职责边界 | 核心优势 |
|---|---|---|
| DateFormatter | 专门处理 DateTime 与 String 的互相转换 | 统一全局的日期显示格式,修改一处即可生效全端 |
| ValidatorUtils | 封装所有正则表达式与输入合法性校验 | 避免在 UI 层书写冗长的校验逻辑,提高表单代码可读性 |
| LoggerService | 封装终端打印与错误日志上报机制 | 在生产环境中能自动屏蔽无用日志,提升运行效率 |
13: 领域驱动的工具隔离策略
通过建立独立的工具类矩阵,我们将所有非 UI 且纯碎的数据转换逻辑抽取出来。这种做法不仅保证了视图层代码的纯粹性,降低了耦合度,更为后期可能引入的多语言架构(i18n)留下了极其平滑的扩展接口。复杂状态过滤与派生流架构
随着任务数据的不断累积,单一的列表展示已经无法满足高效管理的需求。我们需要引入多维度的视图切换机制,例如“全部”、“已完成”和“待办”过滤,以及实时的关键字搜索。在传统的 setState 模式下,这往往意味着要在 UI 层维护大量的临时变量与复杂的逻辑判断。而借助于 Riverpod 的派生状态(Derived State)特性,我们可以构建出极其优雅的响应式数据流。
多维过滤 Provider 源码实现
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/todo_model.dart';
import 'todo_provider.dart';
enum TodoFilter { all, active, completed }
final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);
final searchQueryProvider = StateProvider<String>((ref) => '');
final filteredTodosProvider = Provider<AsyncValue<List<Todo>>>((ref) {
final filter = ref.watch(todoFilterProvider);
final searchQuery = ref.watch(searchQueryProvider).toLowerCase();
final todoState = ref.watch(todoListProvider);
return todoState.whenData((todos) {
return todos.where((todo) {
final matchesFilter = filter == TodoFilter.all ||
(filter == TodoFilter.completed && todo.isCompleted) ||
(filter == TodoFilter.active && !todo.isCompleted);
final matchesSearch = todo.title.toLowerCase().contains(searchQuery) ||
todo.description.toLowerCase().contains(searchQuery);
return matchesFilter && matchesSearch;
}).toList();
});
});
14: 纯函数的派生魅力
filteredTodosProvider 自身并不直接存储任何数据,它扮演的是一个纯函数的角色。它静默监听底层的 todoListProvider、过滤器状态以及搜索关键字。这三者中任意一个发生微小的改变,派生流都会被瞬间重新计算,并将极其精确的过滤结果推送给 UI 层。
15: WhenData 的无缝衔接
通过调用 AsyncValue 内置的 whenData 方法,我们在处理过滤逻辑时自动继承了底层的加载与异常状态。这意味着如果底层数据库正在加载,过滤流也会向 UI 层透传 Loading 状态,彻底避免了空指针或异步时序错乱的灾难。
搜索与过滤组件的视觉呈现
为了让用户能够无缝切换视图,我们在顶部的 AppBar 下方嵌入一个持久化的搜索与过滤面板。
SearchAndFilterBar 源码实现
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/filter_provider.dart';
import '../../core/constants/app_colors.dart';
class SearchAndFilterBar extends ConsumerWidget {
const SearchAndFilterBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentFilter = ref.watch(todoFilterProvider);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: AppColors.primary,
child: Column(
children: [
TextField(
onChanged: (value) => ref.read(searchQueryProvider.notifier).state = value,
decoration: InputDecoration(
hintText: '搜索任务标题或描述...',
hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
prefixIcon: const Icon(Icons.search, color: Colors.white),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
contentPadding: const EdgeInsets.symmetric(vertical: 0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
),
style: const TextStyle(color: Colors.white),
cursorColor: Colors.white,
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_FilterChip(
label: '全部',
isSelected: currentFilter == TodoFilter.all,
onSelected: () => ref.read(todoFilterProvider.notifier).state = TodoFilter.all,
),
_FilterChip(
label: '待办',
isSelected: currentFilter == TodoFilter.active,
onSelected: () => ref.read(todoFilterProvider.notifier).state = TodoFilter.active,
),
_FilterChip(
label: '已完成',
isSelected: currentFilter == TodoFilter.completed,
onSelected: () => ref.read(todoFilterProvider.notifier).state = TodoFilter.completed,
),
],
),
],
),
);
}
}
class _FilterChip extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback onSelected;
const _FilterChip({
required this.label,
required this.isSelected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return ChoiceChip(
label: Text(label),
selected: isSelected,
onSelected: (_) => onSelected(),
selectedColor: Colors.white,
backgroundColor: Colors.white.withOpacity(0.2),
labelStyle: TextStyle(
color: isSelected ? AppColors.primary : Colors.white,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide.none,
),
);
}
}
16: 沉浸式检索体验
将搜索框与过滤标签页紧密结合在主题色的背景内,打破了传统 Material Design 中生硬的割裂感。搜索操作直接将字符串写入 searchQueryProvider,由于 Riverpod 的极速响应,用户在敲击键盘的瞬间,下方的列表就会完成毫秒级的重绘过滤。
本地数据库的版本迭代与平滑迁移
一个工业级的应用绝不可能一成不变。假设在未来的版本中,我们需要为每个任务增加一个“优先级”字段。直接修改数据模型会导致旧版本用户的应用在打开时触发 SQL 解析异常。因此,我们必须在底层构建严密的数据库迁移(Migration)机制。
| 数据库版本 | 核心变更 | 迁移策略 |
|---|---|---|
| Version 1 | 初始建表 | 执行完整的 CREATE TABLE 语句 |
| Version 2 | 新增优先级列 | 通过 ALTER TABLE 动态追加列,设置默认值保证旧数据兼容 |
| Version 3 | 新增分类表 | 创建外键关联,建立索引优化联表查询效率 |
DatabaseHelper 迁移逻辑改造
Dart
// 在 DatabaseHelper 类中更新以下方法
static const String columnPriority = 'priority'; // 新增字段:1-低, 2-中, 3-高
Future<Database> _initDatabase() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, 'todo_pro.db');
return await openDatabase(
path,
version: 2, // 版本号升级为 2
onCreate: _onCreate,
onUpgrade: _onUpgrade, // 注入升级回调
);
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// 从版本 1 升级到版本 2:追加优先级字段,默认为 1(低优先级)
await db.execute('''
ALTER TABLE $tableTodos
ADD COLUMN $columnPriority INTEGER NOT NULL DEFAULT 1;
''');
}
// 预留未来版本升级的梯形控制流
// if (oldVersion < 3) { ... }
}
17: 无损升维降维打击
通过 onUpgrade 回调拦截旧版本数据库的挂载过程,我们使用 ALTER TABLE 在原有表结构末尾追加字段。赋予 DEFAULT 1 是点睛之笔,它确保了用户在更新 App 后,过往创建的所有旧任务依然能完美解析,数据实现了真正的无损跨越。
隐式动画与交互微反馈
当用户点击复选框完成一个任务时,如果 UI 只是冷冰冰地瞬间改变状态,会极大削弱软件的获得感。Flutter 强大的动画系统允许我们以极低的性能开销植入交互反馈。我们将使用 AnimatedSwitcher 对勾选状态进行包装。
TodoItemTile 动画进阶源码
Dart
// 修改 TodoItemTile 中的 leading 部分
leading: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: Checkbox(
key: ValueKey<bool>(todo.isCompleted), // 强制刷新动画的关键
value: todo.isCompleted,
activeColor: AppColors.success,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
onChanged: (_) {
ref.read(todoListProvider.notifier).toggleCompleted(todo.id);
},
),
),
18: ValueKey 驱动的组件新生
在 AnimatedSwitcher 的子组件中强制注入基于布尔值的 ValueKey。当任务完成状态发生反转时,Flutter 的渲染树比对算法会发现 Key 发生了彻底的改变,从而判定这是一个全新的 Widget,随即触发我们设定的缩放与淡入淡出(Scale & Fade)组合动画,让任务完成的过程充满仪式感。数据聚合与可视化图表引擎
当我们积累了海量的任务数据后,单一的列表已无法直观反映用户的执行效率。引入数据可视化面板,是提升 TODO 应用商业价值的杀手锏。为了实现优雅的图表渲染,我们在 pubspec.yaml 中追加工业级图表库 fl_chart: ^0.65.0。我们将构建一个按周统计的任务完成度折线图。
这不仅要求 UI 层面的绘制,更需要在 Riverpod 中构建一个专门用于数据聚合的计算流。
| 图表维度 | 数据源提取逻辑 | 视觉映射规则 |
|---|---|---|
| X轴 (时间线) | 获取当前日期的前7天,按天分组 | 映射为底部的周一至周日文本 |
| Y轴 (完成量) | 筛选 isCompleted == true 且在对应日期内的任务总数 |
映射为折线图中具体的纵向坐标点 |
| 趋势线 | 将每日的完成量平滑连接 | 使用贝塞尔曲线填充渐变色,增强科技感 |
StatisticsProvider 聚合计算源码
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/todo_model.dart';
import 'todo_provider.dart';
final weeklyStatsProvider = Provider<AsyncValue<List<int>>>((ref) {
final todoState = ref.watch(todoListProvider);
return todoState.whenData((todos) {
final now = DateTime.now();
final List<int> dailyCompletions = List.filled(7, 0);
for (var todo in todos) {
// 仅统计已完成的任务
if (!todo.isCompleted) continue;
// 计算任务完成日期与今天的差值
final difference = now.difference(todo.createdAt).inDays;
// 如果是在过去7天内完成的
if (difference >= 0 && difference < 7) {
// 数组索引 6 代表今天,0 代表7天前
final index = 6 - difference;
dailyCompletions[index]++;
}
}
return dailyCompletions;
});
});
19: 时间窗口的动态裁剪
通过 DateTime.now().difference 方法,我们在内存中瞬间剥离出不在目标统计周期内的冗余数据。这种基于内存的过滤方式在处理万级别以下的数据时,远比重新发起 SQL 的 GROUP BY 查询要高效,极大减轻了磁盘 I/O 压力。
20: 响应式图表重绘机制
由于 weeklyStatsProvider 同样是基于 todoListProvider 派生出来的,这意味着当用户在首页勾选完成一个任务的瞬间,图表提供者会立即重新计算数组,图表 UI 也会随之发生丝滑的动态形变,整个过程无需任何手动触发。
WeeklyChartWidget 图表组件源码
Dart
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/statistics_provider.dart';
import '../../core/constants/app_colors.dart';
class WeeklyChartWidget extends ConsumerWidget {
const WeeklyChartWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final statsState = ref.watch(weeklyStatsProvider);
return Card(
margin: const EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'近7日高效指数',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
),
const SizedBox(height: 24),
SizedBox(
height: 200,
child: statsState.when(
data: (data) => LineChart(_buildChartData(data)),
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('数据加载异常')),
),
),
],
),
),
);
}
LineChartData _buildChartData(List<int> data) {
final maxY = data.reduce((curr, next) => curr > next ? curr : next).toDouble() + 2;
return LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) => FlLine(
color: Colors.grey.withOpacity(0.2),
strokeWidth: 1,
),
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
const days = ['前6天', '前5天', '前4天', '前3天', '前2天', '昨天', '今天'];
if (value.toInt() >= 0 && value.toInt() < days.length) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(days[value.toInt()], style: const TextStyle(fontSize: 10, color: Colors.grey)),
);
}
return const Text('');
},
),
),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 6,
minY: 0,
maxY: maxY,
lineBarsData: [
LineChartBarData(
spots: List.generate(data.length, (index) => FlSpot(index.toDouble(), data[index].toDouble())),
isCurved: true,
color: AppColors.primary,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppColors.primary.withOpacity(0.15),
),
),
],
);
}
}
21: 动态极值探测与坐标轴自适应
在 _buildChartData 中,我们使用 reduce 高阶函数动态探测当前一周内的最大完成量,并将其加 2 作为 Y 轴的 maxY。这彻底避免了固定坐标轴导致的折线溢出或由于数值过小导致的图表贴底,让无论任务多少,折线始终处于最佳视觉黄金分割位。
22: 贝塞尔曲线与区域渲染
通过设置 isCurved: true 和 belowBarData,我们将生硬的点阵数据转化为极具现代感的平滑曲线,并在底部渲染出具有透明度的渐变区域。这种微小的视觉处理,是将普通开源项目提升至商业级应用质感的关键跨越。
本地通知与系统级调度
一个优秀的任务管理应用不仅要被动记录,更要主动提醒。通过引入 flutter_local_notifications 依赖,我们能够绕过繁琐的后端推送服务,直接在操作系统底层注册闹钟事件。即使用户彻底杀死了应用进程,系统依然能在设定的截止时间准时唤醒通知模块。
NotificationService 核心调度引擎
Dart
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import '../../data/models/todo_model.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
tz.initializeTimeZones();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestSoundPermission: true,
requestBadgePermission: true,
requestAlertPermission: true,
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _notificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
// 后续可扩展:点击通知直接跳转到特定任务的详情页
print('Notification clicked: ${response.payload}');
},
);
}
Future<void> scheduleTodoReminder(Todo todo) async {
if (todo.dueDate == null || todo.isCompleted) return;
// 提前30分钟提醒
final scheduledTime = todo.dueDate!.subtract(const Duration(minutes: 30));
if (scheduledTime.isBefore(DateTime.now())) return;
final androidPlatformChannelSpecifics = AndroidNotificationDetails(
'todo_reminders_channel',
'任务提醒',
channelDescription: '用于提醒即将到期的任务',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
color: const Color(0xFF2563EB),
);
final platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: const DarwinNotificationDetails(),
);
// 将 UUID 字符串的哈希值作为通知的唯一整型 ID,方便后续取消
final notificationId = todo.id.hashCode;
await _notificationsPlugin.zonedSchedule(
notificationId,
'任务即将到期',
'您的任务 "${todo.title}" 将在30分钟后截止,请及时处理。',
tz.TZDateTime.from(scheduledTime, tz.local),
platformChannelSpecifics,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
payload: todo.id,
);
}
Future<void> cancelReminder(String todoId) async {
await _notificationsPlugin.cancel(todoId.hashCode);
}
}
23: 时区隔离与精确制导
时间处理在跨平台开发中是极度危险的雷区。通过引入 timezone 库并调用 tz.initializeTimeZones(),我们将设备本地时间强制转化为绝对的国际时区对象 TZDateTime。这确保了即使用户带着手机跨越了国家时区,通知的触发瞬间也绝对不会发生任何偏移。
24: 低功耗模式下的强制唤醒
在 zonedSchedule 方法中,我们特意配置了 AndroidScheduleMode.exactAllowWhileIdle 参数。这赋予了我们的应用在 Android 系统处于 Doze(深度休眠)模式时,依然能够强制穿透系统封锁并点亮屏幕的特权,保证了提醒机制的绝对可靠性。
应用入口生命周期注入
为了让通知服务生效,我们必须将其挂载到应用的极早期启动序列中,并在 Riverpod 的 TodoNotifier 中埋点,确保每次创建、更新或删除任务时,底层的通知调度队列也会同步更新。
入口初始化与 Notifier 联动
Dart
// 在 main.dart 的 main 方法中追加
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final notificationService = NotificationService();
await notificationService.initialize();
runApp(const ProviderScope(child: TodoApp()));
}
// 在 TodoNotifier 中的 addTodo 和 toggleCompleted 方法中追加通知逻辑
Future<void> addTodo(String title, String description, DateTime? dueDate) async {
// ... 之前的代码 ...
try {
await _dbHelper.insertTodo(newTodo);
// 新增通知调度逻辑
await NotificationService().scheduleTodoReminder(newTodo);
// ... 之前的代码 ...
}
}
Future<void> toggleCompleted(String id) async {
// ... 之前的代码 ...
if (todoIndex != -1) {
final todo = currentTodos[todoIndex];
final updatedTodo = todo.copyWith(isCompleted: !todo.isCompleted);
try {
await _dbHelper.updateTodo(updatedTodo);
// 核心联动:如果任务已完成,立即取消底层闹钟;如果重新标记为未完成,则重新调度
if (updatedTodo.isCompleted) {
await NotificationService().cancelReminder(updatedTodo.id);
} else {
await NotificationService().scheduleTodoReminder(updatedTodo);
}
// ... 之前的代码 ...
}
}
}
25: 状态与调度的原子化绑定
我们将底层操作系统的定时器管理与 Riverpod 的状态变更彻底绑定。当任务状态发生翻转时(例如误触完成又取消),通知调度器会自动执行取消再重新挂载的逻辑闭环。这意味着 UI 状态即系统级状态,开发者彻底告别了幽灵通知的困扰。
Sliver 架构重构与极致性能滚动
随着我们向首页加入搜索栏、统计图表以及海量的任务列表,传统的 Column 嵌套 ListView 架构将面临极其严峻的渲染瓶颈。页面外不可视的图表组件会被强制保存在内存中,滚动时引发可怕的掉帧。
引入 CustomScrollView 与 Sliver 系列组件,是打破性能天花板的终极武器。我们将重写 HomeScreen 的骨架。
重构后的 HomeScreen (Sliver 核心架构)
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/filter_provider.dart';
import '../widgets/todo_item_tile.dart';
import '../widgets/weekly_chart_widget.dart';
import 'add_todo_sheet.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 120.0,
floating: true,
pinned: true,
elevation: 0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('任务控制中心', style: TextStyle(fontWeight: FontWeight.bold)),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Theme.of(context).primaryColor, Colors.blueAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
// 后续扩展:设置页面
},
)
],
),
// 将搜索与过滤面板包装为 SliverToBoxAdapter
const SliverToBoxAdapter(
child: SearchAndFilterBar(),
),
// 将图表包装为 SliverToBoxAdapter
const SliverToBoxAdapter(
child: WeeklyChartWidget(),
),
// 核心列表区域使用 SliverList 保证高性能复用
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
sliver: _buildSliverTodoList(ref),
),
// 底部留白,防止 FAB 遮挡最后一条数据
const SliverToBoxAdapter(
child: SizedBox(height: 80),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showAddTodoSheet(context),
icon: const Icon(Icons.add),
label: const Text('新建任务'),
),
);
}
Widget _buildSliverTodoList(WidgetRef ref) {
final filteredTodosState = ref.watch(filteredTodosProvider);
return filteredTodosState.when(
data: (todos) {
if (todos.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Center(
child: Column(
children: [
Icon(Icons.inbox_outlined, size: 80, color: Colors.grey.shade300),
const SizedBox(height: 16),
Text('当前视图暂无任务', style: TextStyle(color: Colors.grey.shade500, fontSize: 16)),
],
),
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return TodoItemTile(todo: todos[index]);
},
childCount: todos.length,
),
);
},
loading: () => const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator())),
error: (e, _) => SliverToBoxAdapter(child: Center(child: Text('出错了: $e'))),
);
}
void _showAddTodoSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const AddTodoSheet(),
);
}
}
26: Sliver 视口级别的动态按需渲染
使用 SliverList 搭配 SliverChildBuilderDelegate,系统底层的渲染引擎只会绘制当前屏幕可视范围内及其上下缓冲区的列表项。一旦任务滑出屏幕,对应的内存对象会立即被引擎回收。即使列表中积压了上万条数据,滑动过程依然能保持极其惊艳的 60FPS 到 120FPS 刷新率。
27: 沉浸式的空间伸缩力学
SliverAppBar 配合 floating: true 和 pinned: true 属性,创造出了一种极富生命力的交互反馈。当用户向下滑动查看更多任务时,头部区域会自动折叠以让出极其宝贵的屏幕空间;而当用户向上回拽的瞬间,应用栏又会如同具有弹性一般立刻浮现,提供随时可用的操作入口。