0. 属性传值

对于稍微复杂一点的、尤其视图层级比较深的 UI 样式,一个属性可能需要跨越很多层才能传递给子组件,这种传递方式就会导致中间很多并不需要这个属性的组件也需要接收其子 Widget 的数据,不仅繁琐而且冗余。

1. InheritedWidget

InheritedWidget 是 Flutter 中的一个功能型 Widget,适用于在 Widget 树中共享数据的场景。通过它,我们可以高效地将数据在 Widget 树中进行跨层传递。

  1. class CountContainer extends InheritedWidget {
  2. //方便其子Widget在Widget树中找到它
  3. static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
  4. final int count;
  5. CountContainer({
  6. Key key,
  7. @required this.count,
  8. @required Widget child,
  9. }): super(key: key, child: child);
  10. // 判断是否需要更新
  11. @override
  12. bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
  13. }


  1. class _MyHomePageState extends State<MyHomePage> {
  2. @override
  3. Widget build(BuildContext context) {
  4. //将CountContainer作为根节点,并使用0作为初始化count
  5. return CountContainer(
  6. count: 0,
  7. child: Counter()
  8. );
  9. }
  10. }
  11. class Counter extends StatelessWidget {
  12. @override
  13. Widget build(BuildContext context) {
  14. //获取InheritedWidget节点
  15. CountContainer state = CountContainer.of(context);
  16. return Scaffold(
  17. appBar: AppBar(title: Text("InheritedWidget demo")),
  18. body: Text(
  19. 'You have pushed the button this many times: ${state.count}',
  20. ),
  21. );
  22. }

可以看到 InheritedWidget 的使用方法还是比较简单的,无论 Counter 在 CountContainer 下层什么位置,都能获取到其父 Widget 的计数属性 count,再也不用手动传递属性了。

不过,InheritedWidget 仅提供了数据读的能力,如果我们想要修改它的数据,则需要把它和 StatefulWidget 中的 State 配套使用。我们需要把 InheritedWidget 中的数据和相关的数据修改方法,全部移到 StatefulWidget 中的 State 上,而 InheritedWidget 只需要保留对它们的引用。

  1. class CountContainer extends InheritedWidget {
  2. ...
  3. final _MyHomePageState model;//直接使用MyHomePage中的State获取数据
  4. final Function() increment;
  5. CountContainer({
  6. Key key,
  7. @required this.model,
  8. @required this.increment,
  9. @required Widget child,
  10. }): super(key: key, child: child);
  11. ...
  12. }
  1. class _MyHomePageState extends State<MyHomePage> {
  2. int count = 0;
  3. void _incrementCounter() => setState(() {count++;});//修改计数器
  4. @override
  5. Widget build(BuildContext context) {
  6. return CountContainer(
  7. model: this,//将自身作为model交给CountContainer
  8. increment: _incrementCounter,//提供修改数据的方法
  9. child:Counter()
  10. );
  11. }
  12. }
  13. class Counter extends StatelessWidget {
  14. @override
  15. Widget build(BuildContext context) {
  16. //获取InheritedWidget节点
  17. CountContainer state = CountContainer.of(context);
  18. return Scaffold(
  19. ...
  20. body: Text(
  21. 'You have pushed the button this many times: ${state.model.count}', //关联数据读方法
  22. ),
  23. floatingActionButton: FloatingActionButton(onPressed: state.increment), //关联数据修改方法
  24. );
  25. }
  26. }

2. Notification

Notification 是 Flutter 中进行跨层数据共享的另一个重要的机制。如果说 InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递,那 Notificaiton 则恰恰相反,数据流动方式是从子 Widget 向上传递至父 Widget。这样的数据传递机制适用于子 Widget 状态变更,发送通知上报的场景。

  1. class CustomNotification extends Notification {
  2. CustomNotification(this.msg);
  3. final String msg;
  4. }
  5. //抽离出一个子Widget用来发通知
  6. class CustomChild extends StatelessWidget {
  7. @override
  8. Widget build(BuildContext context) {
  9. return RaisedButton(
  10. //按钮点击时分发通知
  11. onPressed: () => CustomNotification("Hi").dispatch(context),
  12. child: Text("Fire Notification"),
  13. );
  14. }
  15. }
  1. class _MyHomePageState extends State<MyHomePage> {
  2. String _msg = "通知:";
  3. @override
  4. Widget build(BuildContext context) {
  5. //监听通知
  6. return NotificationListener<CustomNotification>(
  7. onNotification: (notification) {
  8. setState(() {_msg += notification.msg+" ";});//收到子Widget通知,更新msg
  9. },
  10. child:Column(
  11. mainAxisAlignment: MainAxisAlignment.center,
  12. children: <Widget>[Text(_msg),CustomChild()],//将子Widget加入到视图树中
  13. )
  14. );
  15. }
  16. }

3. EventBus

Flutter 跨组件传数据的 3 种方法 - 图1

  1. 属性传值:单页面同一个视图树中使用,或者通过构造方法将值传递过去,有点直接将值带过去,不需要过多的操作,缺点是多层级的Widget需要一层层的传值,效率很低;中间一层忘了传整个下游都中断,而且中间某一个层级修改了数据,上层无法及时更新;
  2. InheritedWidget:主要体现是下层Widget主动去向上层拿数据,实现相对复杂,缺点传值方向的单一;
  3. Notification:与InheritedWidget相反,主要体现推数据,针对性强,具体通知给哪个Widget明确,不需要跨多层实现,缺点实现起来相对繁琐点,传值方向单一;
  4. EventBus:订阅关系,针对性强,全局使用,缺点是不同的事件需要定义不同的实体,传递时要区分哪个事件传递给哪个控件,销毁Widget时不能忘记取消订阅;

Provider

我们可以发现,通过观察者模式来实现跨组件状态共享有一些明显的缺点:

  1. 必须显式定义各种事件,不好管理
  2. 订阅者必须需显式注册状态改变回调,也必须在组件销毁时手动去解绑回调以避免内存泄露。

在Flutter当中有没有更好的跨组件状态管理方式了呢?答案是肯定的,那怎么做的?我们想想前面介绍的InheritedWidget,它的天生特性就是能绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可,Flutter社区著名的Provider包正是基于这个思想实现的一套跨组件状态共享解决方案。

Flutter 跨组件传数据的 3 种方法 - 图2

现在Flutter社区已经有很多专门用于状态管理的包了,在此我们列出几个相对评分比较高的:

包名 介绍
Provider & Scoped Model 这两个包都是基于InheritedWidget的,原理相似
Redux 是Web开发中React生态链中Redux包的Flutter实现
MobX 是Web开发中React生态链中MobX包的Flutter实现
BLoC 是BLoC模式的Flutter实现