Flutter中InheritedWidget和Prodiver

InheritedWidget是Flutter中一个比较基础但重要的概念,本文主要整理InheritedWidget的使用及注意事项,以及了解如何基于InheritedWidget实现Provider,此外还顺带学习了Notification,了解在Flutter中如何实现跨组件共享数据和通信的方法。

<!--more-->

1. InheritedWidget

在React或者Vue中,父子组件的通信可以通过props及自定义事件实现,对于嵌套较深的祖先组件和后台组件而言,框架提供了Context获取祖先节点的数据,在多语言、主题等需求下很有用

参考

Flutter提供了InheritedWidget实现从上到下跨级传递数据的功能

class ShareDataWidget extends InheritedWidget {
  ShareDataWidget({@required this.data, Widget child}) : super(child: child);
  final int data; // 需要在子树中共享的数据,保存点击次数
}

1.1. 获取祖先节点共享数据

既然是共享数据,首先就得先获取到祖先组件的数据。后代组件可以通过BuildContext.inheritFromWidgetOfExactType(parentWidgetType)向上找到最近的指向类型的组件,该方法会返回父组件实例,这样就可以使用他们上面的数据了。

v1.12.1版本之后,建议通过dependOnInheritedWidgetOfExactType来实现

// 定义一个会在 ShareDataWidget 子树中使用 ShareDataWidget.of(context).data的 子节点
class _TestWidget extends StatefulWidget {
  @override
  __TestWidgetState createState() => __TestWidgetState();
}

class __TestWidgetState extends State<_TestWidget> {
  @override
  Widget build(BuildContext context) {
    // 使用InheritedWidget中的共享数据
    return Text(context.dependOnInheritedWidgetOfExactType<ShareDataWidget>().data.toString());
  }
}

上面在build方法中获取祖先实例的代码太长了,按照惯例,ShareDataWidget需要提供一个静态的of方法

class ShareDataWidget extends InheritedWidget {
  // ...
  //定义一个便捷方法,方便子树中的widget获取共享数据
  static ShareDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }
}
// 子组件通过ShareDataWidget.of(context).data.toString()访问属性即可

这样,当_TestWidgetShareDataWidget的子节点时,就可以正确获取到依赖数据data

class InheritedWidgetTestRoute extends StatefulWidget {
  @override
  _InheritedWidgetTestRouteState createState() => _InheritedWidgetTestRouteState();
}

class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return ShareDataWidget(
      //使用ShareDataWidget
      data: count,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          _TestWidget(),
          RaisedButton(
            child: Text("Increment"),
            //每点击一次,将count自增,然后重新build,ShareDataWidget的data将被更新
            onPressed: () => setState(() => ++count),
          )
        ],
      ),
    );
  }
}

1.2. 通知数据变化与监听

上面的代码暂时还无法运行,因为InheritedWidget要求子类实现一个updateShouldNotify方法,类似于React类组件中的shouldComponentUpdate,决定是否通知依赖子组件更新。

class ShareDataWidget extends InheritedWidget {
  // ...
  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(ShareDataWidget old) {
    // 如果返回true,则子树中依赖(build函数中有调用)本widget的子widget的`state.didChangeDependencies`会被调用
    return old.data != data;
  }
}

当补完这个方法之后,上面的代码就可以正确运行了;此外,当count发生变化时,_TestWidget会重新执行build并更新视图。

同时,每个继承自StatefulWidget的组件有一个didChangeDependencies回调方法,当其依赖的组件的数据发生变化时会执行,大部分场景下我们不需要重写该方法,不过可以在这里面进行一些逻辑操作。

class __TestWidgetState extends State<_TestWidget> {
  // ...
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 当依赖的InheritedWidget其updateShouldNotify返回true时会被调用
    // 如果build中没有依赖InheritedWidget,则此回调不会被调用。
    print("Dependencies change");
  }
}

1.3. 注册依赖的细节

上面提到,如果__TestWidgetState的build方法中没有使用依赖ShareDataWidget.of(context),则即使ShareDataWidget发生更新,也不会触发didChangeDependencies

class __TestWidgetState extends State<_TestWidget> {
  @override
  Widget build(BuildContext context) {
    // return Text(ShareDataWidget.of(context).data.toString());
    return Text('1');
  }
  // 不会再触发didChangeDependencies
}

接下来看看context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()这行代码的具体作用

abstract class BuildContext {
  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    // 看起来就是这里把InheritedWidget添加到当前buildContext的_dependencie列表中
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    // 同时将当前context添加到InheritedWidget的订阅列表里面,方便通知变化
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
}

如此看来,当发生变化时,InheritedWidget会通过其订阅列表通知所有订阅了数据的后代组件进行更新。(Widget与Element对应的关系目前只是大致了解,后面会继续学习并整理的~)

考虑到这样一种场景,某个子组件只希望获取InheritedWidget的初始数据,而不希望后续接收后续消息通知,那么该如何实现呢?只需要将dependOnInheritedWidgetOfExactType替换为getElementForInheritedWidgetOfExactType即可

static ShareDataWidget of(BuildContext context) {
  return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}

查看源码可以看见,该方法只是把最近的InheritedWidget返回,未执行依赖添加相关逻辑。

@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}

2. Provider

之前写过一篇在Flutter中封装redux的使用,实际上使用redux需要些很多模板代码,对于小项目而言不是很方便,接下来看看官方推荐的状态管理工具Provider

首先安装依赖

provider: ^4.1.3

2.1. 定义Model

接着是定义Model,一个Model主要包括一些需要共享的数据,已经更新这些数据的方法。

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

这里的Model需要使用ChangeNotifier混合,ChangeNotifier实现了包括事件订阅addListener和通知notifyListeners等相关的API。

2.2. 在Widget中使用Model

在使用Model之前需要先进行注册,一般可以使用MultiProviderChangeNotifierProviderListenableProvider

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => Counter()),
  ],
  child: MyApp(), // 内部会包含Count等使用Model及FloatingActionButton等更新Model的组件
),

然后定义一个使用Model的组件

class Count extends StatelessWidget {
  const Count({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 通过watch调用,这样可以在Model更新的时候重新触发build
    return Text(
        '${context.watch<Counter>().count}',
        style: Theme.of(context).textTheme.headline4);
  }
}

这样,当在其他地方更新Model中的数据时

FloatingActionButton(
  // 这里使用的是context.read而不是context.watch,依次Model变化时不会更新改组件
  onPressed: () => context.read<Counter>().increment(),
  tooltip: 'Increment',
  child: const Icon(Icons.add),
)

Count的数据就会自动更新啦~看起来比redux写的代码少很多。

2.3. 大致实现原理

参考

上一章节学习了ineritedWidget中祖先Widget与后代Widget共享数据,让人不禁联想ProviderineritedWidget有啥关系。

以最简单的ChangeNotifierProvider注册单个Model为例

ChangeNotifierProvider(
  create: (_) => Counter(),
  child: MyApp(),)

大体思路为

  • ineritedWidget组件提供的data数据类型设置为Model
  • 由于Model混合了ChangeNotifier,因此在ChangeNotifierProvider的内部挂载child的容器组件中,可以先获取data并调用data.addListener注册事件,当数据更新时重新渲染child
  • 由于ineritedWidget的特点,在子组件child中,如果通过xx.of使用数据,则当祖先节点的数据更新时,会自动触发并更新子节点
    • context.watch<Counter>类似于前面的dependOnInheritedWidgetOfExactType,会获取祖先节点的数据Model,同时添加依赖方便数据更新时通知对应依赖节点
    • context.read<Counter>()类似于前面的getElementForInheritedWidgetOfExactType,只获取数据Model,不添加依赖

具体的源码细节这里并没有展开因为我也出于刚学的阶段...待后面补充吧

3. Notification

本来这篇文章主要是整理ineritedWidget和Provider的,突然发现貌似少了点啥,因此这里一并整理一下Notification

在跨节点通信场景中,可以把InheritedWidget看做是从上到下传递数据,我们还需要一种从下到上通信的机制,Notification提供了相关功能。

在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。参考:Notification

Notification与浏览器中的事件冒泡比较相似,可以在上层节点的任意位置监听通知,也可以阻止冒泡,不再继续向上通知。

实现通知需要两个步骤。第一步是定义自定义的Notification

class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}

然后需要使用NotificationListener注册onNotification回调,最后在NotificationListener的后代组件中通过Notification实例的dispatch(context)方法触发通知

class NotificationRoute extends StatefulWidget {
  @override
  NotificationRouteState createState() {
    return new NotificationRouteState();
  }
}

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //监听通知
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        setState(() {
          _msg+=notification.msg+"  ";
        });
        return true;
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            // 这种写法无法触发外层的onNotification,因为使用的context是NotificationRouteState的buildContext,
            // 而不是NotificationListener后代组件的buildContext
//          RaisedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),
            Builder(
              builder: (context) {
                // 需要使用builder获取NotificationListener后代组件的buildContext
                return RaisedButton(
                  // 通过Notification实例的dispatch方法触发通知
                  onPressed: () => MyNotification("Hi").dispatch(context),
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}

这样,当dispatch(context)时,父组件的NotificationListener<MyNotification>就会收到对应的通知并执行onNotification回调了。

NotificationListener可以嵌套监听,并且当onNotification方法返回true时会阻止通知继续冒泡

NotificationListener<MyNotification>(
  onNotification: (notification) {
    setState(() {
      _msg += notification.msg + "  ";
    });
    return true;
  },
  child: NotificationListener<MyNotification>(
    onNotification: (notification) {
      print('stop notification');
      return true; // 返回true阻止冒泡,外层的NotificationListener将无法收到通知,也就不会执行onNotification了
    },
    child: Builder(
      builder: (context) {
        return RaisedButton(
          //按钮点击时分发通知
          onPressed: () => MyNotification("Hi").dispatch(context),
          child: Text("Send Notification"),
        );
      },
    ),
  ),
)

4. 小结

本文主要总结了Flutter中InheritedWidgetNotification的基本用法,了解了在Flutter中

  • 通过InheritedWidget向下与后代组件共享数据
  • 了解使用Provider这种轻便的数据状态管理库
  • 通过Notification向上通知祖先组件NotificationListener监听事件

发现Flutter里面有很多思想跟Web是互通的,另外Flutter的源码并不如想象中的晦涩难懂,有机会的话应该从上而下整体过一遍,从顶层设计再深入某些功能的具体实现。