父子组件之间的通信
最简单的方式就是通过构造器传递数据,但当嵌套层级比较深的时,传参就比较难看了,或许这时还考虑用单例,但对于简单数据来说未免大材小用了。
例如,还是这个二级页场景,由多个子孙组件组成(同色框表示同级),在顶层页面可以知道从上个页面携带的数据,但最下层的输入框组件(箭头位置)也需要知道部分数据才能发送评论,这里说下如何用Provider快速实现。
Provider
介绍:基于InheritedWidget,一种自上向下的共享数据机制,方便子孙节点得到祖先节点共享的数据。
举例:
// 1、添加依赖
dependencies:
provider: ^5.0.0
// 2、通过Provider包裹的child都共享数据 feedShareData
...
return Scaffold(
...
body: Provider(
create: (_) => feedShareData,
child: Builder(builder: (context) {
return FeedDetailPage();
}),
),
);
// 3、在子孙组件都可以读取并修改共享数据
feedShareData = context.read<NormalFeedsDetailShareData>();
...
适用场景:当子组件需要共享祖先/父组件的数据时,尤其在跨层传递场景。
除了数据共享外,还有就是数据传递,也就是组件想知道另个组件的某些变化,并做出反应(callback)。在Dart中函数是可以当做对象传递的,但同样存在层级复杂时可能要传递大量的函数,接下来介绍常用的数据传递组件。
ChangeNotifier
介绍:可用于扩展Model,并在Model发生变化时调用notifyListeners使得监听该Model的组件重新构建。
举例:详见官方提供的案例,实现了加入购物车的效果。
适用场景: 常和Provider(Consumer)配套解决数据共享&状态传递问题。
NotificationListener
介绍:通知机制,即每个节点都可以向上发送Notification(自下向上的),除非中途被拦截否则会一直向上传。
举例:在上篇Flutter小白|初探事件处理介绍了如何通过NotificationListener解决多个滑动组件下、监听最外层组件滑动到边界事件,可见使用NotificationListener可以很方便获取到可滑动子组件的滑动行为。当然还可以在子组件上通过Notification#dispatch发送自定义通知,然后在上层接收并处理。
// 1、自定义通知类
class CustomNotification extends Notification {
String msg;
CustomNotification(this.msg);
}
// 2、子组件发送通知
...
CustomNotification("Test").dispatch(context);
...
// 3、父组件接收和处理通知
...
NotificationListener<CustomNotification>(
onNotification: (notification) {
// 处理逻辑
// 同样的返回值表示是否拦截
return true;
},
child:...
)
适用场景:当祖先/父组件要随着子组件的某些情况变化而变化时,常用于滑动情况。
兄弟组件之间的通信
ValueListenableBuilder
介绍:顾名思义,对某个 value进行监听,只要value发生变化就会直接通过setState重新构建子组件树。同样的,因为会rebuild,使用时尽可能缩小包裹的组件范围,以减少不必要的组件被重建、提高性能。
举例:例如想要实现这样的需求,底部输入框有内容时按钮亮起,无内容按钮置灰,代码如下:
// 1、创建:定义一个文本内容的ValueNotifier,
ValueNotifier<String> emojiInfoNotifier = ValueNotifier("");
// 2、使用:输入框组件和按钮组件都监听这个string信息
...
ValueListenableBuilder(
valueListenable: emojiInfoNotifier,
builder: (BuildContext context, String value, Widget? child) {
// 输入框组件,根据输入内容展示,没有内容展示默认文案
return Text(
value.isEmpty
? Strings.share_thoughts
: value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: Res.t12, color: Res.c1),
);
},
),
...
ValueListenableBuilder(
valueListenable: emojiInfoNotifier,
builder: (BuildContext context, String value, Widget? child) {
// 按钮组件,根据有无内容改变icon颜色
return SvgPicture.asset(
iconUrl,
color: value.isEmpty ? Res.c4 : Res.c2,
width: 16,
height: 16,
);
},
),
...
// 3、随数据变化而变化:当点击表情按钮导致文本内容发生变化时,输入框组件和按钮组件都会发生变化,
...
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
// 超过最大表情展示个数会弹toast提示
if (emojiInfoNotifier.value.characters.length >= maxInputEmojiCount) {
Toasts.show(Strings.input_more);
} else {
emojiInfoNotifier.value += emoji?.text ?? "";
}
},
child:... // 表情按钮
)...
// 4、销毁:在依赖组件dispose的同时销毁
@override
void dispose() {
super.dispose();
emojiInfoNotifier.dispose();
...
}
适用场景:当组件A要随着其他组件B上操作的数据变化而变化时,可以在同个组件树,也可以不是。常用于组件关系不复杂且监听数据简单的情况。
EventBus
介绍:对于要跨组件甚至跨页面组件通信的,这时用EventBus屡试不爽。关于 EventBus 开发一定非常熟悉了,核心就是观察者模式,一方发布消息,一方订阅感兴趣的消息并做处理。
举例:
// 1、添加依赖
dependencies:
event_bus: ^2.0.0
// 2、新增事件类
class CustomEvent {
String msg;
CustomEvent(this.msg);
}
// 3、发送事件(用单例维护,这里不展示了)
EventBusExt.getDefault().fire(CustomEvent("test"));
// 4、订阅事件并处理
EventBusExt.getDefault().on<CustomEvent>().listen((event) {
// 处理逻辑
});
适用场景:可以在任何位置传递事件,可以解决大多数通信需求。
总结
本篇从数据通信方式(共享数据、传递数据)和组件关系(父子关系、兄弟关系)两方面分别介绍了Flutter中常用的组件,整体看Provider和EventBus总是能很好的处理各类场景,功能强大,但也有维护成本的问题(比如要新增很多事件类等),一些简单场景可以考虑其他小组件。