- 属性传值
- 对于数据的跨层传递,Flutter 还提供了三种方案:InheritedWidget、Notification 和 EventBus。
InheritedWidget
InheritedWidget 是 Flutter 中的一个功能型 Widget,适用于在 Widget 树中共享数据的场景。通过它,我们可以高效地将数据在 Widget 树中进行跨层传递。
Theme 类是通过 InheritedWidget 实现的典型案例.
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}',
),
);
}
- InheritedWidget 仅提供了数据读的能力,如果我们想要修改它的数据,则需要把它和 StatefulWidget 中的 State 配套使用。我们需要把 InheritedWidget 中的数据和相关的数据修改方法,全部移到 StatefulWidget 中的 State 上,而 InheritedWidget 只需要保留对它们的引用。
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 作为根节点,完成了数据和修改方法的初始化。
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), //关联数据修改方法
);
}
}
Notification
如果说 InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递,那 Notificaiton 则恰恰相反,数据流动方式是从子 Widget 向上传递至父 Widget。这样的数据传递机制适用于子 Widget 状态变更,发送通知上报的场景。
- 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加入到视图树中
)
);
}
}
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"))
),
);
}
}
总结
图 5 属性传值、InheritedWidget、Notification 与 EventBus 数据传递方式对比