Skip to content

4种跨组件传递数据的方式

shenchunxing edited this page Aug 25, 2023 · 1 revision

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

属性

比较直接,但是在跨多页面传递会比较麻烦,需要一层层传递

InheritedWidget(跨多个父子节点方便,但修改数据麻烦)

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

Theme类是通过InheritedWidget实现的典型案例。在子Widget中通过Theme.of方法找到上层Theme的Widget,获取到其属性的同时,建立子Widget和上层父Widget的观察者关系,当上层父Widget属性修改的时候,子Widget也会触发更新。

接下来,我就以Flutter工程模板中的计数器为例,与你说明InheritedWidget的使用方法。

  • 首先,为了使用InheritedWidget,我们定义了一个继承自它的新类CountContainer。
  • 然后,我们将计数器状态count属性放到CountContainer中,并提供了一个of方法方便其子Widget在Widget树中找到它。
  • 最后,我们重写了updateShouldNotify方法,这个方法会在Flutter判断InheritedWidget是否需要重建,从而通知下层观察者组件更新数据时被调用到。在这里,我们直接判断count是否相等即可。
class CountContainer extends InheritedWidget {
  //方便其子Widget在Widget树中找到它
  static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
  
  final int count;

  CountContainer({
    Key key,
    @required this.count,
    @required Widget child,
  }): super(key: key, child: child);

  // 判断是否需要更新
  @override
  bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}

然后,我们使用CountContainer作为根节点,并用0初始化count。随后在其子Widget Counter中,我们通过InheritedCountContainer.of方法找到它,获取计数状态count并展示:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
   //将CountContainer作为根节点,并使用0作为初始化count
    return CountContainer(
      count: 0,
      child: Counter()
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //获取InheritedWidget节点
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${state.count}',
      ),
    );
}

运行一下,效果如下图所示:

image

图1 InheritedWidget使用方法

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

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

我们对上面的代码稍加修改,删掉CountContainer中持有的count属性,增加对数据持有者State,以及数据修改方法的引用:

class CountContainer extends InheritedWidget {
  ...
  final _MyHomePageState model;//直接使用MyHomePage中的State获取数据
  final Function() increment;

  CountContainer({
    Key key,
    @required this.model,
    @required this.increment,
    @required Widget child,
  }): super(key: key, child: child);
  ...
}

然后,我们将count数据和其对应的修改方法放在了State中,仍然使用CountContainer作为根节点,完成了数据和修改方法的初始化。

在其子Widget Counter中,我们还是通过InheritedCountContainer.of方法找到它,将计数状态count与UI展示同步,将按钮的点击事件与数据修改同步:

class _MyHomePageState extends State<MyHomePage> {
  int count = 0;
  void _incrementCounter() => setState(() {count++;});//修改计数器

  @override
  Widget build(BuildContext context) {
    return CountContainer(
      model: this,//将自身作为model交给CountContainer
      increment: _incrementCounter,//提供修改数据的方法
      child:Counter()
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //获取InheritedWidget节点
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      ...
      body: Text(
        'You have pushed the button this many times: ${state.model.count}', //关联数据读方法
      ),
      floatingActionButton: FloatingActionButton(onPressed: state.increment), //关联数据修改方法
    );
  }
}

运行一下,可以看到,我们已经实现InheritedWidget数据的读写了。

image

图2 InheritedWidget数据修改示例

Notification(传递事件方便,读取不方便)

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

在前面的第13篇文章“经典控件(二):UITableView/ListView在Flutter中是什么?”中,我与你介绍了ScrollNotification的使用方法:ListView在滚动时会分发通知,我们可以在上层使用NotificationListener监听ScrollNotification,根据其状态做出相应的处理。

自定义通知的监听与ScrollNotification并无不同,而如果想要实现自定义通知,我们首先需要继承Notification类。Notification类提供了dispatch方法,可以让我们沿着context对应的Element节点树向上逐层发送通知。

接下来,我们一起看一个具体的案例吧。在下面的代码中,我们自定义了一个通知和子Widget。子Widget是一个按钮,在点击时会发送通知:

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

//抽离出一个子Widget用来发通知
class CustomChild extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //按钮点击时分发通知
      onPressed: () => CustomNotification("Hi").dispatch(context),
      child: Text("Fire Notification"),
    );
  }
}

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

class _MyHomePageState extends State<MyHomePage> {
  String _msg = "通知:";
  @override
  Widget build(BuildContext context) {
    //监听通知
    return NotificationListener<CustomNotification>(
        onNotification: (notification) {
          setState(() {_msg += notification.msg+"  ";});//收到子Widget通知,更新msg
        },
        child:Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[Text(_msg),CustomChild()],//将子Widget加入到视图树中
        )
    );
  }
}

运行一下代码,可以看到,我们每次点击按钮之后,界面上都会出现最新的通知信息:

image

图3 自定义Notification

EventBus(通过事件总线,全局,方便,但要记住事件名称,全局容易冲突,组件消除要清理事件。)

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

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

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

dependencies:  
  event_bus: 1.1.0

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

class CustomEvent {
  String msg;
  CustomEvent(this.msg);
}

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

//建立公共的event bus
EventBus eventBus = new EventBus();
//第一个页面
class _FirstScreenState extends  State<FirstScreen>  {

  String msg = "通知:";
  @override
  initState() {
   //监听CustomEvent事件,刷新UI
    eventBus.on<CustomEvent>().listen((event) {
      setState(() {msg+= event.msg;});//更新msg
    });
    super.initState();
  }
  dispose() {
    subscription.cancel();//State销毁时,清理注册
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body:Text(msg),
      ...
    );
  }
}

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

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      ...
      body: RaisedButton(
          child: Text('Fire Event'),
          // 触发CustomEvent事件
          onPressed: ()=> eventBus.fire(CustomEvent("hello"))
      ),
    );
  }
}

运行一下,多点击几下第二个页面的按钮,然后返回查看第一个页面上的消息:

image

图4 EventBus示例

可以看到,EventBus的使用方法还是比较简单的,使用限制也相对最少。

这里我准备了一张表格,把属性传值、InheritedWidget、Notification与EventBus这四种数据共享方式的特点和使用场景做了简单总结,供你参考:

image

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

总结

好了,今天的分享就到这里。我们来简单回顾下在Flutter中,如何实现跨组件的数据共享。

首先,我们认识了InheritedWidget。对于视图层级比较深的UI样式,直接通过属性传值的方式会导致很多中间层增加冗余属性,而使用InheritedWidget可以实现子Widget跨层共享父Widget的属性。需要注意的是,InheritedWidget中的属性在子Widget中只能读,如果有修改的场景,我们需要把它和StatefulWidget中的State配套使用。

然后,我们学习了Notification,这种由下到上传递数据的跨层共享机制。我们可以使用NotificationListener,在父Widget监听来自子Widget的事件。

最后,我与你介绍了EventBus,这种无需发布者与订阅者之间存在父子关系的数据同步机制。

Clone this wiki locally