查看: 143|回复: 3

鸿蒙Flutter开发:Provider状态管理集成与踩坑总结

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
在 Flutter 的状态管理方案里,我比较过 Bloc、GetX 和 Riverpod。Bloc 的事件/状态类太多,写起来繁琐;GetX 功能强大但侵入性强;Riverpod 概念多,学习曲线陡。最后还是选了 Provider,它简单、够用,而且实现是纯 Dart。

纯 Dart 这点对鸿蒙适配很重要。项目要跑在 HarmonyOS 上,部分依赖三方原生代码的 package 可能没有鸿蒙实现,但 Provider 不依赖平台代码,所以加依赖直接编译通过,没有遇到任何适配问题。

我在 Demo 里主要验证了两种场景:计数器展示基础用法,待办列表展示跨组件共享。后续还规划了网络请求状态、购物车共享、主题切换持久化、多步骤表单等方向,都会围绕 Provider 推进。

接下来把集成过程和关键用法整理成文,分享给有类似需求的开发者。

集成 Provider

我用的是 provider 6.1.5+1,在 pubspec.yaml 中加上依赖即可:
  1. dependencies:
  2.   flutter:
  3.     sdk: flutter
  4.   provider: ^6.1.5+1
复制代码

执行 flutter pub get 后就能用,既不需要配置 iOS/Android,鸿蒙编译也没报错。

基于 ChangeNotifier 的状态容器

Provider 最常见的搭档是 ChangeNotifier。思路是:创建一个继承 ChangeNotifier 的类,内部保存数据,数据变更时调用 notifyListeners()。

计数器模型:
  1. class CounterModel extends ChangeNotifier {
  2.   int _count = 0;
  3.   int get count => _count;
  4.   void increment() {
  5.     _count++;
  6.     notifyListeners();
  7.   }
  8.   void reset() {
  9.     _count = 0;
  10.     notifyListeners();
  11.   }
  12. }
复制代码

第一次写时很容易漏掉 notifyListeners()。我曾在按钮回调里更新了 count,断点确认数据确实变了,但界面没有反应,排查半天才发现忘了调用通知方法。

待办列表模型也类似:
  1. class TodoModel extends ChangeNotifier {
  2.   final List<String> _items = ['学习 Flutter', '写鸿蒙应用', '发布文章'];
  3.   List<String> get items => List.unmodifiable(_items);
  4.   void add(String text) {
  5.     _items.add(text);
  6.     notifyListeners();
  7.   }
  8.   void removeAt(int index) {
  9.     _items.removeAt(index);
  10.     notifyListeners();
  11.   }
  12. }
复制代码

这里有个细节:对外暴露不可变列表,所有修改都通过 Model 的方法。我之前把 List 直接暴露出去,外部调用 add() 后数据变了 UI 不刷新,因为没用 notifyListeners()。统一走 Model 方法就不会漏。

依赖注入与作用域

在 widget 树顶层通过 MultiProvider 注入多个 Model:
  1. MultiProvider(
  2.   providers: [
  3.     ChangeNotifierProvider(create: (_) => CounterModel()),
  4.     ChangeNotifierProvider(create: (_) => TodoModel()),
  5.   ],
  6.   child: const MyApp(),
  7. )
复制代码

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。

比如计数器页面:
  1. class CounterSection extends StatelessWidget {
  2.   const CounterSection();
  3.   @override
  4.   Widget build(BuildContext context) {
  5.     final model = context.watch<CounterModel>();
  6.     return Column(
  7.       children: [
  8.         Text('${model.count}', style: TextStyle(fontSize: 48)),
  9.         Row(
  10.           children: [
  11.             ElevatedButton(
  12.               onPressed: () => context.read<CounterModel>().increment(),
  13.               child: Text('+1'),
  14.             ),
  15.             OutlinedButton(
  16.               onPressed: () => context.read<CounterModel>().reset(),
  17.               child: Text('归零'),
  18.             ),
  19.           ],
  20.         ),
  21.       ],
  22.     );
  23.   }
  24. }
复制代码

如果按钮也使用 watch,点击后整个 widget 会 rebuild,造成性能浪费。

context.watch 不能在 initState 中使用,因为此时依赖关系尚未建立。需要初始化时读取数据,可以先用 context.read 读一次,再在 build 中用 watch 建立监听。

如果只想让部分 widget 响应变化,可以使用 Consumer:
  1. Consumer<CounterModel>(
  2.   builder: (context, model, child) {
  3.     return Text('${model.count}');
  4.   },
  5. )
复制代码

Consumer 只 rebuild builder 内的部分,外层不受影响;页面整体跟随 Model 变化时则可以直接用 context.watch。

跨组件同步案例

待办列表 Demo 展示两个独立 widget 共享同一个 TodoModel。输入区添加待办:
  1. Row(
  2.   children: [
  3.     Expanded(
  4.       child: TextField(
  5.         controller: _textCtrl,
  6.         decoration: InputDecoration(hintText: '输入新待办'),
  7.       ),
  8.     ),
  9.     ElevatedButton(
  10.       onPressed: () {
  11.         if (_textCtrl.text.trim().isNotEmpty) {
  12.           context.read<TodoModel>().add(_textCtrl.text.trim());
  13.           _textCtrl.clear();
  14.         }
  15.       },
  16.       child: Text('添加'),
  17.     ),
  18.   ],
  19. )
复制代码

列表展示区:
  1. final model = context.watch<TodoModel>();
  2. ListView(
  3.   children: model.items.map((item) {
  4.     return ListTile(
  5.       title: Text(item),
  6.       trailing: IconButton(
  7.         icon: Icon(Icons.delete_outline),
  8.         onPressed: () => context.read<TodoModel>().removeAt(index),
  9.       ),
  10.     );
  11.   }).toList(),
  12. )
复制代码

数据流向很清晰:View 触发 Action,Model 更新数据并通知,监听者自动刷新。不需要手动 setState,也不需要父子传值。

与 ArkTS 状态管理的对比

我也写过鸿蒙原生 ArkTS,两者状态管理思路差异明显。

ArkTS 使用装饰器驱动:
  1. @Component
  2. struct CounterPage {
  3.   @State count: number = 0;
  4.   build() {
  5.     Column() {
  6.       Text(`${this.count}`)
  7.       Button('+1').onClick(() => {
  8.         this.count++
  9.       })
  10.     }
  11.   }
  12. }
复制代码

@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:
  1. Selector<CounterModel, int>(
  2.   selector: (_, model) => model.count,
  3.   builder: (_, count, __) => Text('$count'),
  4. )
复制代码

其他字段变化不会触发 rebuild,只有 count 变化才会重建,能减少不必要的刷新。

ProxyProvider 用于 Model 之间的依赖,比如购物车模型依赖用户模型中的 userId:
  1. ProxyProvider<UserModel, ShoppingCartModel>(
  2.   update: (_, user, cart) => cart!..updateUser(user),
  3. )
复制代码

踩坑汇总

做 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 分三层:
  1. StateDemo (顶层, MultiProvider 注入)
  2. └── _StateApp (Scaffold + ListView)
  3.     ├── _CounterSection (watch CounterModel)
  4.     └── _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 问题,欢迎交流。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙Flutter开发:Provider状态管理集成与踩坑总结

感谢楼主分享,很实用!特别是“纯 Dart 对鸿蒙适配很重要”这点,我深有体会。之前我们也评估过几个状态管理库,有些依赖了原生通道,在鸿蒙上确实要额外处理,Provider 这种纯 Dart 实现就省心很多,编译直接过,不用折腾桥接。 你提到的忘记调 `notifyListeners()` 的坑我也踩过,后来养成了习惯:所有修改状态的方法里,只要数据变了,第一反应就是检查有没有通知。另外 Provider 的位置确实关键,包在 `MaterialApp` 外侧是最稳妥的做法,不然页面跳转后就拿不到实例了。 关于 `watch` 和 `read` 的区分,我自己的理解是:`watch` 用于“这个数据变了我要刷新界面”,`read` 用于“我只需要触发一个方法,不需要重建”。楼主说的“读数据用 watch,发命令用 read”很形象,我后来就是这么给团队新人讲的。 期待你后续分享网络请求状态、购物车这些场景的实践,跟着学习了!
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙Flutter开发:Provider状态管理集成与踩坑总结

感谢分享!Provider 确实是最省心的选择,纯 Dart 这点对鸿蒙适配太关键了。我也踩过忘记调 `notifyListeners()` 的坑,排查半天最后发现是这个问题,当时真想捶自己。你提到对外暴露不可变列表这个细节很实用,我之前的做法是直接返回内部 List,结果外部改了也不刷新,后来改成和你一样的方式就正常了。另外 `.value` 的生命周期管理也提醒得好,确实容易忽略。 期待你后续写网络请求状态和主题持久化的实践,到时候再来学习。
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙Flutter开发:Provider状态管理集成与踩坑总结

感谢楼主分享,这篇对正在搞鸿蒙 Flutter 的开发者很有参考价值。Provider 确实简单实用,纯 Dart 这点在鸿蒙适配时省心不少,能避开原生依赖的坑。 踩坑总结也很到位,`notifyListeners()` 漏掉、Provider 作用域位置不对、`watch` 和 `read` 混淆,这三个基本是新手必踩的雷。楼主把 `context.watch` 和 `context.read` 区分成“读数据”和“发命令”,这个总结很形象。 想请教一下,楼主用 `MultiProvider` 是不是为了后续扩展购物车、主题持久化这些状态?另外待办列表里 `List.unmodifiable` 对外暴露不可变列表这点,实际体验中会不会有性能损耗?希望后续能分享更多网络请求和主题切换的实践。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-10 22:35 , Processed in 0.024091 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部