You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
14 KiB
Markdown

2 years ago
# 20 | 关于跨组件传递数据,你只需要记住这三招
你好,我是陈航。
在上一篇文章中我带你一起学习了在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主题](https://time.geekbang.org/column/article/112148)”中我与你介绍了如何通过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}',
),
);
}
```
运行一下,效果如下图所示:
![](https://static001.geekbang.org/resource/image/da/2e/da4662202739ac00969b111d1c30f12e.png)
图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数据的读写了。
![](https://static001.geekbang.org/resource/image/d1/35/d18d85bdd7d8ccf095535db077f3cd35.gif)
图2 InheritedWidget数据修改示例
## Notification
Notification是Flutter中进行跨层数据共享的另一个重要的机制。如果说InheritedWidget的数据流动方式是从父Widget到子Widget逐层传递那Notificaiton则恰恰相反数据流动方式是从子Widget向上传递至父Widget。这样的数据传递机制适用于子Widget状态变更发送通知上报的场景。
在前面的第13篇文章“[经典控件UITableView/ListView在Flutter中是什么](https://time.geekbang.org/column/article/110859)”中我与你介绍了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加入到视图树中
)
);
}
}
```
运行一下代码,可以看到,我们每次点击按钮之后,界面上都会出现最新的通知信息:
![](https://static001.geekbang.org/resource/image/e8/dd/e8c504730c0580dc699a0807039c76dd.gif)
图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"))
),
);
}
}
```
运行一下,多点击几下第二个页面的按钮,然后返回查看第一个页面上的消息:
![](https://static001.geekbang.org/resource/image/5a/d5/5a50441b322568f4f3d77aea87d2aed5.gif)
图4 EventBus示例
可以看到EventBus的使用方法还是比较简单的使用限制也相对最少。
这里我准备了一张表格把属性传值、InheritedWidget、Notification与EventBus这四种数据共享方式的特点和使用场景做了简单总结供你参考
![](https://static001.geekbang.org/resource/image/b2/66/b2a78dbefdf30895504b2017355ae066.png)
图5 属性传值、InheritedWidget、Notification与EventBus数据传递方式对比
## 总结
好了今天的分享就到这里。我们来简单回顾下在Flutter中如何实现跨组件的数据共享。
首先我们认识了InheritedWidget。对于视图层级比较深的UI样式直接通过属性传值的方式会导致很多中间层增加冗余属性而使用InheritedWidget可以实现子Widget跨层共享父Widget的属性。需要注意的是InheritedWidget中的属性在子Widget中只能读如果有修改的场景我们需要把它和StatefulWidget中的State配套使用。
然后我们学习了Notification这种由下到上传递数据的跨层共享机制。我们可以使用NotificationListener在父Widget监听来自子Widget的事件。
最后我与你介绍了EventBus这种无需发布者与订阅者之间存在父子关系的数据同步机制。
我把今天分享所涉及到的三种跨组件的[数据共享方式demo](https://github.com/cyndibaby905/20_data_transfer)放到了GitHub你可以下载下来自己运行体会它们之间的共同点和差异。
## 思考题
最后,我来给你留下一个思考题吧。
请你分别概括属性传值、InheritedWidget、Notification与EventBus的优缺点。
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。