极客时间已完结课程限时免费阅读

20 | 关于跨组件传递数据,你只需要记住这三招

20 | 关于跨组件传递数据,你只需要记住这三招-极客时间

20 | 关于跨组件传递数据,你只需要记住这三招

讲述:陈航

时长09:54大小9.07M

你好,我是陈航。
在上一篇文章中,我带你一起学习了在 Flutter 中如何响应用户交互事件(手势)。手势处理在 Flutter 中分为两种:原始的指针事件处理和高级的手势识别。
其中,指针事件以冒泡机制分发,通过 Listener 完成监听;而手势识别则通过 Gesture 处理。但需要注意的是,虽然 Flutter 可以同时支持多个手势(包括一个 Widget 监听多个手势,或是多个 Widget 监听同一个手势),但最终只会有一个 Widget 的手势能够响应用户行为。为了改变这一点,我们需要自定义手势,修改手势竞技场对于多手势优先级判断的默认行为。
除了需要响应外部的事件之外,UI 框架的另一个重要任务是,处理好各个组件之间的数据同步关系。尤其对于 Flutter 这样大量依靠组合 Widget 的行为来实现用户界面的框架来说,如何确保数据的改变能够映射到最终的视觉效果上就显得更为重要。所以,在今天这篇文章中,我就与你介绍在 Flutter 中如何进行跨组件数据传递。
在之前的分享中,通过组合嵌套的方式,利用数据对基础 Widget 的样式进行视觉属性定制,我们已经实现了多种界面布局。所以,你应该已经体会到了,在 Flutter 中实现跨组件数据传递的标准方式是通过属性传值。
但是,对于稍微复杂一点的、尤其视图层级比较深的 UI 样式,一个属性可能需要跨越很多层才能传递给子组件,这种传递方式就会导致中间很多并不需要这个属性的组件也需要接收其子 Widget 的数据,不仅繁琐而且冗余。
所以,对于数据的跨层传递,Flutter 还提供了三种方案:InheritedWidget、Notification 和 EventBus。接下来,我将依次为你讲解这三种方案。

InheritedWidget

InheritedWidget 是 Flutter 中的一个功能型 Widget,适用于在 Widget 树中共享数据的场景。通过它,我们可以高效地将数据在 Widget 树中进行跨层传递。
在前面的第 16 篇文章“从夜间模式说起,如何定制不同风格的 App 主题?”中,我与你介绍了如何通过 Theme 去访问当前界面的样式风格,从而进行样式复用的例子,比如 Theme.of(context).primaryColor。
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}',
),
);
}
运行一下,效果如下图所示:
图 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 数据的读写了。
图 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加入到视图树中
)
);
}
}
运行一下代码,可以看到,我们每次点击按钮之后,界面上都会出现最新的通知信息:
图 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 = "通知:";
StreamSubscription subscription;
@override
initState() {
//监听CustomEvent事件,刷新UI
subscription = 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"))
),
);
}
}
运行一下,多点击几下第二个页面的按钮,然后返回查看第一个页面上的消息:
图 4 EventBus 示例
可以看到,EventBus 的使用方法还是比较简单的,使用限制也相对最少。
这里我准备了一张表格,把属性传值、InheritedWidget、Notification 与 EventBus 这四种数据共享方式的特点和使用场景做了简单总结,供你参考:
图 5 属性传值、InheritedWidget、Notification 与 EventBus 数据传递方式对比

总结

好了,今天的分享就到这里。我们来简单回顾下在 Flutter 中,如何实现跨组件的数据共享。
首先,我们认识了 InheritedWidget。对于视图层级比较深的 UI 样式,直接通过属性传值的方式会导致很多中间层增加冗余属性,而使用 InheritedWidget 可以实现子 Widget 跨层共享父 Widget 的属性。需要注意的是,InheritedWidget 中的属性在子 Widget 中只能读,如果有修改的场景,我们需要把它和 StatefulWidget 中的 State 配套使用。
然后,我们学习了 Notification,这种由下到上传递数据的跨层共享机制。我们可以使用 NotificationListener,在父 Widget 监听来自子 Widget 的事件。
最后,我与你介绍了 EventBus,这种无需发布者与订阅者之间存在父子关系的数据同步机制。
我把今天分享所涉及到的三种跨组件的数据共享方式 demo放到了 GitHub,你可以下载下来自己运行,体会它们之间的共同点和差异。

思考题

最后,我来给你留下一个思考题吧。
请你分别概括属性传值、InheritedWidget、Notification 与 EventBus 的优缺点。
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 5

提建议

上一篇
19 | 用户交互事件该如何响应?
下一篇
21 | 路由与导航,Flutter是这样实现页面切换的
 写留言

精选留言(23)

  • Mr.J
    2019-08-14
    请你分别概括属性传值、InheritedWidget、Notification 与 EventBus 的优缺点。 ·属性传值:单页面同一个视图树中使用,或者通过构造方法将值传递过去,有点直接将值带过去,不需要过多的操作,缺点是多层级的Widget需要一层层的传值,效率很低;中间一层忘了传整个下游都中断,而且中间某一个层级修改了数据,上层无法及时更新; ·InheritedWidget:主要体现是下层Widget主动去向上层拿数据,实现相对复杂,(有个疑问,多层的视图树,在最下层直接使用of可以获取到最上层的数据吗?),缺点传值方向的单一; ·Notification:与InheritedWidget相反,主要体现推数据,针对性强,具体通知给哪个Widget明确,不需要跨多层实现,缺点实现起来相对繁琐点,传值方向单一; ·EventBus:订阅关系,针对性强,全局使用,缺点是不同的事件需要定义不同的实体,传递时要区分哪个事件传递给哪个控件,销毁Widget时不能忘记取消订阅;
    展开

    作者回复: 赞 InheritedWidget 无论跨多少层都可以的

    共 3 条评论
    15
  • 许童童
    2019-08-13
    老师能讲下闲鱼的Redux吗?

    作者回复: 更推荐Provider

    共 5 条评论
    13
  • 许童童
    2019-08-13
    请你分别概括属性传值、InheritedWidget、Notification 与 EventBus 的优缺点。 属性传值:简单,但跨多个父子节点不方便 InheritedWidget:跨多个父子节点方便,但修改数据麻烦 Notification :传递事件方便,读取不方便 EventBus :通过事件总线,全局,方便,但要记住事件名称,全局容易冲突,组件消除要清理事件。
    展开

    作者回复: 赞

    8
  • 寂寞不点烟
    2019-12-17
    subscription = eventBus.on().listen((event) { setState(() {msg+= event.msg;});//更新msg });event这样接收会出现事件混乱。应该加一层判断 if(event.runtimeType == CustomEvent){ setState(() { msg += event.msg; }); //更新msg }
    展开

    作者回复: 赞

    1
  • 淡~
    2019-08-14
    Bloc和scopedmodel等一些状态管理方案会讲吗,更富推荐那些做状态管理啊

    作者回复: 推荐Provider

    1
  • 福杯满溢
    2022-06-23
    为什么把属性和修改属性的方法放在InheritedWidget里不可以呢?子组件获取父组件,同样可以调用父组件里的方法修改属性呀。
  • J
    2021-08-13
    老师您好,我带着 MVVM 和 MVC 的想法学习本节内容,您将到的 InheritedWidget 这个跨层功能 widget 的 demo ,看到了一些 数据驱动 页面的影子,但是整体的数据流转和页面展示似乎不大清晰,可以指点一下 flutter 的 MVC 或者 MVVM 的代码组织是怎样的吗?
  • Geek_061196
    2021-08-10
    随着项目的迭代, Eventbus 会越来越不方便维护。有没有类似阿里的Arouter组件,替代Eventbus实现组件间通信呢?
  • 毛成方
    2021-02-01
    在实际项目中遇到问题的时候 翻阅这里的文章 真的很有帮助 绝对是干货
  • Chvjs
    2020-08-11
    现在 StreamSubscription 是 undefined 了,API 更新了吗?
  • Geek_6b80e0
    2020-04-22
    属性传值,是只能通过层层传递,容易丢失和不好使用。 InheritedWidget 可以解决跨层传递,灵活性比较大,需要根据不同的需求实现of逻辑。只能从父级往子级进行传递。 Notification 其实也是通过订阅模式,进行传递,需要子级触发,父级订阅。 EventBus 全局共享,没有父子限制。
  • JimLai
    2019-12-30
    老师您好,请问flutter可以达到类似安卓ContentProvider实现的跨进程数据通讯的效果吗?例如:一个由flutter开发的app部署到安卓设备上,与另一个原生安卓app进行数据通讯。
  • 🌝
    2019-11-13
    我通过子widget类直接修改传入的父widget中的变量可以吗?实验过,如果是对象的变量可以直接修改掉。 class Obj { int a; String b; Obj({this.a, this.b}); } class OneWidget extends StatefulWidget { OneWidget({Key key}) : super(key: key); @override _OneWidgetState createState() => _OneWidgetState(); } class _OneWidgetState extends State<OneWidget> { Obj obj; @override Widget build(BuildContext context) { return TwoWidget(obj: obj); } } class TwoWidget extends StatefulWidget { final Obj obj; TwoWidget({Key key, this.obj}) : super(key: key); @override _TwoWidgetState createState() => _TwoWidgetState(); } class _TwoWidgetState extends State<TwoWidget> { @override Widget build(BuildContext context) { return FlatButton( onPressed: () { widget.obj.a = 123; }, child: Text('点击'), ); } }
    展开

    作者回复: 可以啊,对象是引用传递就没问题

  • 和小胖
    2019-09-10
    老师,dispose() { subscription.cancel();} 这里的 subscription 哪里来的呢?不是应该是 eventbus.destroy() 吗?

    作者回复: 注册通知(调用listen)的时候会返回一个监听对象,用于后续取消事件通知

  • 和小胖
    2019-09-10
    老师,上面提的问题似乎找到答案了。 把 _incrementCounter 传入到 CountContainer 里面或许是为了类似于 java 里面的多态,子类可以有很多,同时子类可以自定义很多自己的方法,但是在调用的时候都统一调用父类的同名方法。 而我用 state.increment 之所以不行,是因为我是 onPressed: () => state.increment 这样写的,如果改成 onPressed: () => state.increment() 其实也是可以的,或者按照老师那种 onPressed 的写法也是可以的。 onPressed: () => state.increment() 是否可以认为是在 onPressed 的回调响应里面调用了 state 的 increment() 方法而已,而 onPressed: state.increment 是不是可以看成是在拿 state 的 increment 属性在给 onPressed 这个属性赋值呢?
    展开

    作者回复: 赞,你已经得出正确结论了

  • 和小胖
    2019-09-10
    老师,请问下第一种父传子的方式,为啥要把 _incrementCounter 传入到CountContainer里面呢?在按钮的点击事件里面直接使用 state.model._incrementCounter() 不是也可以吗? 另外我发现我用state.model._incrementCounter()是可以,但是使用 state.increment 却无法让 数字变化,这是为什么呢?
    展开

    作者回复: 1.直接使用State当然可以呀,只是这个例子是演示如何通过InheritedWidget进行读写数据。InheritedWidget只能读不能写,要写数据得通过State中转一层 2.看评论你已经得出结论啦

  • 咖啡凉了
    2019-09-09
    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), // 关联数据修改方法 ); } } 我在尝试的时候遇到了问题,这段代码中的 state.increment ,没有反应,改成state.increment()才能响应。这是什么问题
    展开

    作者回复: 这段代码应该是没问题的,如果是用箭头函数才会有问题

  • 历史课代表
    2019-09-04
    可以讲一下EventBus在Flutter中怎么实现的吗

    作者回复: EventBus原理比较简单,通过一个可以被多人共享(订阅)的数据流就可以实现事件的分发。Dart的event_bus实际上就是通过一个可以被多个人订阅的Stream实现的,而其底层的StreamController就是专门用来给Steam制造数据的。

  • 看不懂的你
    2019-08-29
    老师具体讲一下provider的实现与思想,还有bloc的sixiabg

    作者回复: Provider会讲的

  • ptlCoder
    2019-08-27
    系统没有提供缓存相关API嘛?

    作者回复: 有啊。但是对于跨组件传递数据,存取方式并不重要,重要的是存好了怎么通知其他组件来取