
前言
Flutter 的状态管理方案琳琅满目------BLoC、Riverpod、Redux、GetX......但对于一个中小型应用,Provider 的"刚好够用"哲学往往是最务实的选择。它没有 BLoC 的样板代码地狱,也没有 Riverpod 的概念曲线,5 分钟就能上手。
鸿蒙 Flutter 备忘录应用使用 MultiProvider + 4 个 ChangeNotifier 管理整个应用的状态。本文拆解这个架构的设计思路、常见陷阱和最佳实践。
项目仓库:todo_flutter_harmony
为什么选 Provider
| 方案 | 学习成本 | 代码量 | 鸿蒙兼容性 | 适合场景 |
|---|---|---|---|---|
| Provider | 低 | 少 | ✅ 纯 Dart | 中小应用 |
| BLoC | 高 | 多 | ✅ 纯 Dart | 大型应用/团队 |
| Riverpod | 中 | 中 | ✅ 纯 Dart | 中大型应用 |
| GetX | 低 | 少 | ⚠️ 框架耦合 | 快速原型 |
| Redux | 高 | 多 | ✅ 纯 Dart | 复杂状态 |
对于这个备忘录应用(4 个数据实体,10 个页面),Provider 是最合适的------学习成本低,鸿蒙天然兼容,且社区庞大。
应用入口:MultiProvider 注册
dart
// main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => MemoProvider()),
ChangeNotifierProvider(create: (_) => TodoProvider()),
ChangeNotifierProvider(create: (_) => DiaryProvider()),
ChangeNotifierProvider(create: (_) => CategoryProvider()),
],
child: const App(),
);
}
}
MultiProvider 的嵌套顺序不影响功能------这 4 个 Provider 之间没有依赖关系,它们各自独立。
如果 Provider 之间有依赖(比如 MemoProvider 需要 CategoryProvider 的数据),可以使用 ProxyProvider:
dart
ChangeNotifierProxyProvider<CategoryProvider, MemoProvider>(
create: (_) => MemoProvider(),
update: (_, categoryProvider, memoProvider) {
memoProvider!.updateCategoryProvider(categoryProvider);
return memoProvider;
},
)
但在本应用中,每个 Provider 都直接访问 DatabaseHelper 单例,没有相互依赖。
MemoProvider 完整实现
dart
class MemoProvider extends ChangeNotifier {
final DatabaseHelper _db = DatabaseHelper.instance;
List<Memo> _allMemos = [];
bool _isSelectionMode = false;
final Set<int> _selectedIds = {};
int? _categoryFilter;
String _searchQuery = '';
// ====== Getters ======
List<Memo> get allMemos => List.unmodifiable(_allMemos);
bool get isSelectionMode => _isSelectionMode;
Set<int> get selectedIds => Set.unmodifiable(_selectedIds);
int get selectedCount => _selectedIds.length;
List<Memo> get filteredMemos {
var result = List<Memo>.from(_allMemos);
if (_categoryFilter != null) {
result = result.where((m) => m.categoryId == _categoryFilter).toList();
}
if (_searchQuery.isNotEmpty) {
final q = _searchQuery.toLowerCase();
result = result.where((m) =>
m.title.toLowerCase().contains(q) ||
m.content.toLowerCase().contains(q)
).toList();
}
result.sort((a, b) {
if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;
return b.createdAt.compareTo(a.createdAt);
});
return result;
}
// ====== 数据加载 ======
Future<void> loadMemos() async {
_allMemos = await _db.getAllMemos();
notifyListeners();
}
// ====== CRUD ======
Future<void> addMemo(Memo memo) async {
await _db.insertMemo(memo);
await loadMemos();
}
Future<void> updateMemo(Memo memo) async {
await _db.updateMemo(memo);
await loadMemos();
}
Future<void> deleteMemo(int id) async {
await _db.deleteMemo(id);
await loadMemos();
}
// ... 选择模式、搜索、筛选等其他方法
}
三种 Consumer 模式的选用
context.watch:监听变化
dart
Consumer<MemoProvider>(
builder: (context, provider, _) {
return Text('共 ${provider.allMemos.length} 条备忘录');
},
)
等价写法:
dart
// 在 build 方法中
final provider = context.watch<MemoProvider>();
return Text('共 ${provider.allMemos.length} 条备忘录');
context.watch 会注册一个 listener,当 Provider 调用 notifyListeners() 时,widget 自动重建。
context.read:一次性读取
dart
FloatingActionButton(
onPressed: () {
// 读取但不监听------适合事件处理
context.read<MemoProvider>().addMemo(newMemo);
},
child: const Icon(Icons.add),
)
context.read 不注册 listener ------只在当前时刻读取 Provider,不会导致 widget 重建。适合在 onPressed、onTap 等事件回调中使用。
context.select:精确订阅
dart
// 只监听 memo 数量变化,不关心内容变化
final count = context.select<MemoProvider, int>(
(provider) => provider.allMemos.length,
);
context.select 只在 selector 返回值变化时重建。当 Provider 通知了变化但 memo 数量没变时,这个 widget 不重建------避免不必要的渲染。
IndexedStack + 页面数据加载
应用使用 IndexedStack 保持 4 个 tab 的页面状态:
dart
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: const [
MemoListPage(),
TodoListPage(),
DiaryListPage(),
StatsPage(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(icon: Icon(Icons.note_alt_outlined), label: '备忘录'),
NavigationDestination(icon: Icon(Icons.checklist_outlined), label: '待办'),
NavigationDestination(icon: Icon(Icons.book_outlined), label: '日记'),
NavigationDestination(icon: Icon(Icons.bar_chart), label: '统计'),
],
),
);
}
}
数据在各页面的 initState 中通过 addPostFrameCallback 加载:
dart
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MemoProvider>().loadMemos();
});
}
addPostFrameCallback 确保首帧已渲染后再触发数据加载,避免在 build 阶段调用 Provider 方法(Flutter 禁止在 build 中修改状态)。
await 后的 mounted 检查
dart
Future<void> _deleteMemo(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: const Text('确定要删除这条备忘录吗?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('删除')),
],
),
);
// 关键:await 后检查 mounted
if (!mounted) return;
if (confirmed != true) return;
await context.read<MemoProvider>().deleteMemo(id);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已删除')),
);
}
showDialog 返回一个 Future,在 await 期间用户可能按了系统返回键导航离开,此时 mounted == false。之后任何调用 setState 或 context 的操作都会抛出异常。每个 await 后检查 mounted 是最基本的防御性编程。
鸿蒙兼容性
Provider 是纯 Dart 包,不包含任何原生代码。它仅依赖 Flutter 的 InheritedWidget 机制和 Dart 的 ChangeNotifier 模式,在 Android、iOS、鸿蒙 OHOS 上行为完全一致。
总结
Provider 状态管理架构的关键决策:
- MultiProvider 注册 4 个 ChangeNotifier:各司其职,无相互依赖
- context.watch / read / select 三模式:按场景选择监听策略
- addPostFrameCallback 触发初始加载:避免在 build 中修改状态
- 每个 await 后检查 mounted:防止异步回调中的 widget 已 dispose 崩溃
完整项目代码见:todo_flutter_harmony