在 Flutter 的状态管理方案里,我比较过 Bloc、GetX 和 Riverpod。Bloc 的事件/状态类太多,写起来繁琐;GetX 功能强大但侵入性强;Riverpod 概念多,学习曲线陡。最后还是选了 Provider,它简单、够用,而且实现是纯 Dart。
纯 Dart 这点对鸿蒙适配很重要。项目要跑在 HarmonyOS 上,部分依赖三方原生代码的 package 可能没有鸿蒙实现,但 Provider 不依赖平台代码,所以加依赖直接编译通过,没有遇到任何适配问题。
我在 Demo 里主要验证了两种场景:计数器展示基础用法,待办列表展示跨组件共享。后续还规划了网络请求状态、购物车共享、主题切换持久化、多步骤表单等方向,都会围绕 Provider 推进。
接下来把集成过程和关键用法整理成文,分享给有类似需求的开发者。
集成 Provider
我用的是 provider 6.1.5+1,在 pubspec.yaml 中加上依赖即可:
- dependencies:
- flutter:
- sdk: flutter
- provider: ^6.1.5+1
复制代码
执行 flutter pub get 后就能用,既不需要配置 iOS/Android,鸿蒙编译也没报错。
基于 ChangeNotifier 的状态容器
Provider 最常见的搭档是 ChangeNotifier。思路是:创建一个继承 ChangeNotifier 的类,内部保存数据,数据变更时调用 notifyListeners()。
计数器模型:
- class CounterModel extends ChangeNotifier {
- int _count = 0;
- int get count => _count;
- void increment() {
- _count++;
- notifyListeners();
- }
- void reset() {
- _count = 0;
- notifyListeners();
- }
- }
复制代码
第一次写时很容易漏掉 notifyListeners()。我曾在按钮回调里更新了 count,断点确认数据确实变了,但界面没有反应,排查半天才发现忘了调用通知方法。
待办列表模型也类似:
- class TodoModel extends ChangeNotifier {
- final List<String> _items = ['学习 Flutter', '写鸿蒙应用', '发布文章'];
- List<String> get items => List.unmodifiable(_items);
- void add(String text) {
- _items.add(text);
- notifyListeners();
- }
- void removeAt(int index) {
- _items.removeAt(index);
- notifyListeners();
- }
- }
复制代码
这里有个细节:对外暴露不可变列表,所有修改都通过 Model 的方法。我之前把 List 直接暴露出去,外部调用 add() 后数据变了 UI 不刷新,因为没用 notifyListeners()。统一走 Model 方法就不会漏。
依赖注入与作用域
在 widget 树顶层通过 MultiProvider 注入多个 Model:
- MultiProvider(
- providers: [
- ChangeNotifierProvider(create: (_) => CounterModel()),
- ChangeNotifierProvider(create: (_) => TodoModel()),
- ],
- child: const MyApp(),
- )
复制代码
Provider 的位置很关键。要实现跨页面共享,Provider 必须包裹在导航之前。我曾经把 Provider 放在某个页面的 build 里,结果跳转后新页面拿不到 Model,因为不在同一个组件树。如果全 App 使用,就包在 MaterialApp 外侧;如果某个模块使用,在该模块根 widget 包一层即可。
Provider 的原理与 watch/read
Provider 底层是对 InheritedWidget 的封装。InheritedWidget 是 Flutter 自带的跨组件传值机制,子组件可以通过 context.dependOnInheritedWidgetOfExactType() 获取父级数据。Provider 在此基础上实现了 Provider<T>,子组件通过 context.watch<T>() 获取数据并建立依赖关系。当 ChangeNotifier 调用 notifyListeners() 后,Provider 触发 InheritedWidget 更新,所有依赖该 Provider 的 widget 自动重建。
因此,context.watch 必须用在 Provider 的子组件里,它只是 InheritedWidget 的语法糖。
如果已有实例而不是新建实例,可以用 Provider<T>.value(value: existingInstance)。但要记住,使用 .value 后实例生命周期要自己管理,Provider 不会自动 dispose。
context.watch 和 context.read 是最容易搞混的 API。watch 会建立依赖关系,数据变化时触发当前 widget rebuild;read 只读取一次,不触发 rebuild。官方文档的说法是:read 是 action,watch 是 data。读数据用 watch,发命令用 read。
比如计数器页面:
- class CounterSection extends StatelessWidget {
- const CounterSection();
- @override
- Widget build(BuildContext context) {
- final model = context.watch<CounterModel>();
- return Column(
- children: [
- Text('${model.count}', style: TextStyle(fontSize: 48)),
- Row(
- children: [
- ElevatedButton(
- onPressed: () => context.read<CounterModel>().increment(),
- child: Text('+1'),
- ),
- OutlinedButton(
- onPressed: () => context.read<CounterModel>().reset(),
- child: Text('归零'),
- ),
- ],
- ),
- ],
- );
- }
- }
复制代码
如果按钮也使用 watch,点击后整个 widget 会 rebuild,造成性能浪费。
context.watch 不能在 initState 中使用,因为此时依赖关系尚未建立。需要初始化时读取数据,可以先用 context.read 读一次,再在 build 中用 watch 建立监听。
如果只想让部分 widget 响应变化,可以使用 Consumer:
- Consumer<CounterModel>(
- builder: (context, model, child) {
- return Text('${model.count}');
- },
- )
复制代码
Consumer 只 rebuild builder 内的部分,外层不受影响;页面整体跟随 Model 变化时则可以直接用 context.watch。
跨组件同步案例
待办列表 Demo 展示两个独立 widget 共享同一个 TodoModel。输入区添加待办:
- Row(
- children: [
- Expanded(
- child: TextField(
- controller: _textCtrl,
- decoration: InputDecoration(hintText: '输入新待办'),
- ),
- ),
- ElevatedButton(
- onPressed: () {
- if (_textCtrl.text.trim().isNotEmpty) {
- context.read<TodoModel>().add(_textCtrl.text.trim());
- _textCtrl.clear();
- }
- },
- child: Text('添加'),
- ),
- ],
- )
复制代码
列表展示区:
- final model = context.watch<TodoModel>();
- ListView(
- children: model.items.map((item) {
- return ListTile(
- title: Text(item),
- trailing: IconButton(
- icon: Icon(Icons.delete_outline),
- onPressed: () => context.read<TodoModel>().removeAt(index),
- ),
- );
- }).toList(),
- )
复制代码
数据流向很清晰:View 触发 Action,Model 更新数据并通知,监听者自动刷新。不需要手动 setState,也不需要父子传值。
与 ArkTS 状态管理的对比
我也写过鸿蒙原生 ArkTS,两者状态管理思路差异明显。
ArkTS 使用装饰器驱动:
- @Component
- struct CounterPage {
- @State count: number = 0;
- build() {
- Column() {
- Text(`${this.count}`)
- Button('+1').onClick(() => {
- this.count++
- })
- }
- }
- }
复制代码
@State 变量变化后 UI 自动更新,另有 @Prop(父传子单向)、@Link(父子双向)、@Provide/@Consume(跨层级共享)。这种方案让状态跟踪对开发者变成了黑盒,不如 Provider 显式调用 notifyListeners() 来得直接。ArkTS 的 @State 不能用在非组件类里,跨页面状态通常借助 AppStorage 或 LocalStorage。Provider 本身就是对象级模型,天然适合全局状态管理。
另一个区别是更新时机。ArkTS 的状态更新是同步的,变量赋值后 UI 立即排队更新;Flutter 在 notifyListeners() 之后,widget rebuild 发生在下一帧,不是立即重绘。如果在 notifyListeners() 后面立刻读取 widget 状态,可能读到旧值,写测试时要特别注意。
Provider 的其他实用模式
Selector 可以只监听 Model 中的某一字段,而不是整个 Model:
- Selector<CounterModel, int>(
- selector: (_, model) => model.count,
- builder: (_, count, __) => Text('$count'),
- )
复制代码
其他字段变化不会触发 rebuild,只有 count 变化才会重建,能减少不必要的刷新。
ProxyProvider 用于 Model 之间的依赖,比如购物车模型依赖用户模型中的 userId:
- ProxyProvider<UserModel, ShoppingCartModel>(
- update: (_, user, cart) => cart!..updateUser(user),
- )
复制代码
踩坑汇总
做 Demo 时整理了几个容易踩的坑。
1. 漏调 notifyListeners。排查时在 setter 打断点或打印,确认数据确实变更;如果数据变了 UI 没更新,基本是漏了通知。
2. Provider 作用域不正确。在 PageA 的 build 里创建 Provider,然后导航到 PageB,PageB 中 watch 拿不到 Model。因为 PageB 在另一个路由,不在作用域内。解决方法是把 Provider 提升到 MaterialApp 或导航栈的公共父节点。
3. watch 导致整棵树 rebuild。如果页面级 build 里 watch 了多个 Model,任意一个变化都会触发整个页面重建。解决方法是将功能区域拆成独立 widget,各自 watch 自己需要的 Model,或使用 Consumer/Selector 精确控制重建范围。
4. hot reload 状态保留但报错。Provider 在 hot reload 时不会重建,状态得以保留;但如果修改了 Model 构造函数签名或字段类型,可能报类型不匹配。此时 hot restart 一下即可。这是 Provider 特意保留状态的设计,不是 bug。若想在 hot reload 时重建,可在 create 中加打印验证。
5. ChangeNotifierProvider.value 的坑。外部传入实例时,如果这个实例还被其他地方持有引用,可能导致多个 Provider 各管各的数据,出现不同步。尽量使用 create 创建实例,避免在外面 new 了再传进来。
6. 多 Model 协作混乱。一个页面同时 watch UserModel、CartModel、OrderModel 等,任何一个变化都会引发整页 rebuild。我的解决方式是拆成独立小 widget,每个只 watch 自己需要的 Model;Model 之间存在依赖时用 ProxyProvider 处理。记住原则:widget 不要 watch 与它无关的 Model。
Demo 的组织方式
我的 state_demo.dart 分三层:
- StateDemo (顶层, MultiProvider 注入)
- └── _StateApp (Scaffold + ListView)
- ├── _CounterSection (watch CounterModel)
- └── _TodoSection (watch TodoModel + read TodoModel)
复制代码
Model 层不依赖任何 Flutter widget,是纯 Dart 逻辑,将来换 Riverpod 或 Bloc 时 Model 层不用动。UI 层两种写法都覆盖了:直接 watch/read 和 StatefulWidget + controller。另外 _CounterSection 是 StatelessWidget 却能响应数据变化,因为 context.watch 会在 Provider 数据变化时重新调用 build 方法。所以 StatelessWidget + watch 完全够用,不必专门为刷新 UI 用 StatefulWidget。
给 ArkTS 转 Flutter 的建议
如果两边都在写,几点个人感受:
- Flutter widget 层级更深,Provider、Consumer、Builder 穿插在布局代码中,刚开始会眼花。可以借助 Flutter Inspector 或 widget 树插件查看层级。
- 不必纠结选型。Provider 足够写中大型应用,核心概念就 Provider、ChangeNotifierProvider、MultiProvider、Consumer、Selector,学一下午就能上手。我当初想一步到位用 Riverpod,花了几天概念还没理清。
- ArkTS 的 @Watch 用于监听某个状态变化,Flutter 中 Model 可以用 addListener,但通常直接用 context.watch。
- @State 是变量级,Provider 是对象级。@State 可以作用在任意变量上,粒度细;ChangeNotifier 作用于对象,任何字段变化都会通知所有监听者。想要细粒度就得用 Selector 或拆分小 Model。
后续规划
接下来准备做几个更贴近实际的方向:用 Provider + Dio/http 封装网络请求,管理 loading/data/error 状态;用 Provider + SharedPreferences 做主题切换与持久化;多步骤表单(如注册流程分三页)用 Provider 共享状态,比页面间传参优雅。
这几个场景完成后,Provider 的常见用法基本就覆盖全了。如果之后遇到更复杂的场景再对比 Riverpod 或 Bloc。
以上就是在鸿蒙 Flutter 开发中使用 Provider 的实践记录。Demo 可以直接运行,入口 StateDemo 即可看到效果。如果社区里有人在鸿蒙上遇到 Provider 问题,欢迎交流。 |