• 属性传值
  • 对于数据的跨层传递,Flutter 还提供了三种方案:InheritedWidget、Notification 和 EventBus。

InheritedWidget

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

Theme 类是通过 InheritedWidget 实现的典型案例.

InheritedWidget 的使用方法。

  1. 首先,为了使用 InheritedWidget,我们定义了一个继承自它的新类 CountContainer。
  2. 然后,我们将计数器状态 count 属性放到 CountContainer 中,并提供了一个 of 方法方便其子 Widget 在 Widget 树中找到它。
  3. 最后,我们重写了 updateShouldNotify 方法,这个方法会在 Flutter 判断 InheritedWidget 是否需要重建,从而通知下层观察者组件更新数据时被调用到。在这里,我们直接判断 count 是否相等即可。
  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. 然后,我们使用 CountContainer 作为根节点,并用 0 初始化 count。随后在其子 Widget Counter 中,我们通过 InheritedCountContainer.of 方法找到它,获取计数状态 count 并展示:
  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. }
  1. 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. 然后,我们将 count 数据和其对应的修改方法放在了 State 中,仍然使用 CountContainer 作为根节点,完成了数据和修改方法的初始化。
  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. }

Notification

如果说 InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递,那 Notificaiton 则恰恰相反,数据流动方式是从子 Widget 向上传递至父 Widget。这样的数据传递机制适用于子 Widget 状态变更,发送通知上报的场景。

  • Notification 类提供了 dispatch 方法,可以让我们沿着 context 对应的 Element 节点树向上逐层发送通知。

在下面的代码中,我们自定义了一个通知和子 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. }

在子 Widget 的父 Widget 中,我们监听了这个通知,一旦收到通知,就会触发界面刷新,展示收到的通知信息:

  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. }

EventBus

无论是 InheritedWidget 还是 Notificaiton,它们的使用场景都需要依靠 Widget 树,也就意味着只能在有父子关系的 Widget 之间进行数据共享。但是,组件间数据传递还有一种常见场景:这些组件间不存在父子关系。这时,事件总线 EventBus 就登场了。

事件总线是在 Flutter 中实现跨组件通信的机制。它遵循发布 / 订阅模式,允许订阅者订阅事件,当发布者触发事件时,订阅者和发布者之间可以通过事件进行交互。发布者和订阅者之间无需有父子关系,甚至非 Widget 对象也可以发布 / 订阅。这些特点与其他平台的事件总线机制是类似的。

接下来,我们通过一个跨页面通信的例子,来看一下事件总线的具体使用方法。需要注意的是,EventBus 是一个第三方插件,因此我们需要在 pubspec.yaml 文件中声明它:

  1. dependencies:
  2. event_bus: 1.1.0

EventBus 的使用方式灵活,可以支持任意对象的传递。所以在这里,我们传输数据的载体就选择了一个有字符串属性的自定义事件类 CustomEvent:

  1. class CustomEvent {
  2. String msg;
  3. CustomEvent(this.msg);
  4. }

然后,我们定义了一个全局的 eventBus 对象,并在第一个页面监听了 CustomEvent 事件,一旦收到事件,就会刷新 UI。需要注意的是,千万别忘了在 State 被销毁时清理掉事件注册,否则你会发现 State 永远被 EventBus 持有着,无法释放,从而造成内存泄漏:

  1. //建立公共的event bus
  2. EventBus eventBus = new EventBus();
  3. //第一个页面
  4. class _FirstScreenState extends State<FirstScreen> {
  5. String msg = "通知:";
  6. StreamSubscription subscription;
  7. @override
  8. initState() {
  9. //监听CustomEvent事件,刷新UI
  10. subscription = eventBus.on<CustomEvent>().listen((event) {
  11. setState(() {msg+= event.msg;});//更新msg
  12. });
  13. super.initState();
  14. }
  15. dispose() {
  16. subscription.cancel();//State销毁时,清理注册
  17. super.dispose();
  18. }
  19. @override
  20. Widget build(BuildContext context) {
  21. return new Scaffold(
  22. body:Text(msg),
  23. ...
  24. );
  25. }
  26. }

最后,我们在第二个页面以按钮点击回调的方式,触发了 CustomEvent 事件:

  1. class SecondScreen extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return new Scaffold(
  5. ...
  6. body: RaisedButton(
  7. child: Text('Fire Event'),
  8. // 触发CustomEvent事件
  9. onPressed: ()=> eventBus.fire(CustomEvent("hello"))
  10. ),
  11. );
  12. }
  13. }

总结

image.png

图 5 属性传值、InheritedWidget、Notification 与 EventBus 数据传递方式对比