Flutter中InheritedWidget和Prodiver
InheritedWidget
是Flutter中一个比较基础但重要的概念,本文主要整理InheritedWidget
的使用及注意事项,以及了解如何基于InheritedWidget
实现Provider
,此外还顺带学习了Notification
,了解在Flutter中如何实现跨组件共享数据和通信的方法。
InheritedWidget
在React或者Vue中,父子组件的通信可以通过props及自定义事件实现,对于嵌套较深的祖先组件和后台组件而言,框架提供了Context获取祖先节点的数据,在多语言、主题等需求下很有用
参考
- https://blog.zhoulujue.com/InheritedWidget/
- 数据共享(InheritedWidget)- flutter中文网,本章节大部分代码均来自于该教程,略作调整
Flutter提供了InheritedWidget
实现从上到下跨级传递数据的功能
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({@required this.data, Widget child}) : super(child: child);
final int data; // 需要在子树中共享的数据,保存点击次数
}
获取祖先节点共享数据
既然是共享数据,首先就得先获取到祖先组件的数据。后代组件可以通过BuildContext.inheritFromWidgetOfExactType(parentWidgetType)
向上找到最近的指向类型的组件,该方法会返回父组件实例,这样就可以使用他们上面的数据了。
在v1.12.1
版本之后,建议通过dependOnInheritedWidgetOfExactType
来实现
// 定义一个会在 ShareDataWidget 子树中使用 ShareDataWidget.of(context).data的 子节点
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => __TestWidgetState();
}
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
// 使用InheritedWidget中的共享数据
return Text(context.dependOnInheritedWidgetOfExactType<ShareDataWidget>().data.toString());
}
}
上面在build方法中获取祖先实例的代码太长了,按照惯例,ShareDataWidget
需要提供一个静态的of
方法
class ShareDataWidget extends InheritedWidget {
// ...
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
}
// 子组件通过ShareDataWidget.of(context).data.toString()访问属性即可
这样,当_TestWidget
是ShareDataWidget
的子节点时,就可以正确获取到依赖数据data
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() => _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return ShareDataWidget(
//使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_TestWidget(),
RaisedButton(
child: Text("Increment"),
//每点击一次,将count自增,然后重新build,ShareDataWidget的data将被更新
onPressed: () => setState(() => ++count),
)
],
),
);
}
}
通知数据变化与监听
上面的代码暂时还无法运行,因为InheritedWidget
要求子类实现一个updateShouldNotify
方法,类似于React类组件中的shouldComponentUpdate
,决定是否通知依赖子组件更新。
class ShareDataWidget extends InheritedWidget {
// ...
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
// 如果返回true,则子树中依赖(build函数中有调用)本widget的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
}
}
当补完这个方法之后,上面的代码就可以正确运行了;此外,当count发生变化时,_TestWidget
会重新执行build并更新视图。
同时,每个继承自StatefulWidget
的组件有一个didChangeDependencies
回调方法,当其依赖的组件的数据发生变化时会执行,大部分场景下我们不需要重写该方法,不过可以在这里面进行一些逻辑操作。
class __TestWidgetState extends State<_TestWidget> {
// ...
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 当依赖的InheritedWidget其updateShouldNotify返回true时会被调用
// 如果build中没有依赖InheritedWidget,则此回调不会被调用。
print("Dependencies change");
}
}
注册依赖的细节
上面提到,如果__TestWidgetState
的build方法中没有使用依赖ShareDataWidget.of(context)
,则即使ShareDataWidget
发生更新,也不会触发didChangeDependencies
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
// return Text(ShareDataWidget.of(context).data.toString());
return Text('1');
}
// 不会再触发didChangeDependencies
}
接下来看看context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()
这行代码的具体作用
abstract class BuildContext {
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
// 看起来就是这里把InheritedWidget添加到当前buildContext的_dependencie列表中
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
// 同时将当前context添加到InheritedWidget的订阅列表里面,方便通知变化
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
}
如此看来,当发生变化时,InheritedWidget
会通过其订阅列表通知所有订阅了数据的后代组件进行更新。(Widget与Element对应的关系目前只是大致了解,后面会继续学习并整理的~)
考虑到这样一种场景,某个子组件只希望获取InheritedWidget
的初始数据,而不希望后续接收后续消息通知,那么该如何实现呢?只需要将dependOnInheritedWidgetOfExactType
替换为getElementForInheritedWidgetOfExactType
即可
static ShareDataWidget of(BuildContext context) {
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
查看源码可以看见,该方法只是把最近的InheritedWidget
返回,未执行依赖添加相关逻辑。
@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
Provider
之前写过一篇在Flutter中封装redux的使用,实际上使用redux需要些很多模板代码,对于小项目而言不是很方便,接下来看看官方推荐的状态管理工具Provider
。
首先安装依赖
provider: ^4.1.3
定义Model
接着是定义Model
,一个Model主要包括一些需要共享的数据,已经更新这些数据的方法。
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
这里的Model需要使用ChangeNotifier
混合,ChangeNotifier
实现了包括事件订阅addListener
和通知notifyListeners
等相关的API。
在Widget中使用Model
在使用Model之前需要先进行注册,一般可以使用MultiProvider
、ChangeNotifierProvider
、ListenableProvider
等
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: MyApp(), // 内部会包含Count等使用Model及FloatingActionButton等更新Model的组件
),
然后定义一个使用Model的组件
class Count extends StatelessWidget {
const Count({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 通过watch调用,这样可以在Model更新的时候重新触发build
return Text(
'${context.watch<Counter>().count}',
style: Theme.of(context).textTheme.headline4);
}
}
这样,当在其他地方更新Model中的数据时
FloatingActionButton(
// 这里使用的是context.read而不是context.watch,依次Model变化时不会更新改组件
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
)
Count的数据就会自动更新啦~看起来比redux写的代码少很多。
大致实现原理
参考
- 跨组件状态共享(Provider),这里介绍了实现一个最小功能的Provider。
- 从零开始的Flutter之旅: Provider
上一章节学习了ineritedWidget
中祖先Widget与后代Widget共享数据,让人不禁联想Provider
与ineritedWidget
有啥关系。
以最简单的ChangeNotifierProvider
注册单个Model为例
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),)
大体思路为
- 将
ineritedWidget
组件提供的data数据类型设置为Model
- 由于Model混合了
ChangeNotifier
,因此在ChangeNotifierProvider
的内部挂载child的容器组件中,可以先获取data并调用data.addListener
注册事件,当数据更新时重新渲染child - 由于
ineritedWidget
的特点,在子组件child中,如果通过xx.of
使用数据,则当祖先节点的数据更新时,会自动触发并更新子节点context.watch<Counter>
类似于前面的dependOnInheritedWidgetOfExactType
,会获取祖先节点的数据Model,同时添加依赖方便数据更新时通知对应依赖节点context.read<Counter>()
类似于前面的getElementForInheritedWidgetOfExactType
,只获取数据Model,不添加依赖
具体的源码细节这里并没有展开~因为我也出于刚学的阶段...待后面补充吧~
Notification
本来这篇文章主要是整理ineritedWidget和Provider的,突然发现貌似少了点啥,因此这里一并整理一下Notification
。
在跨节点通信场景中,可以把InheritedWidget
看做是从上到下传递数据,我们还需要一种从下到上通信的机制,Notification
提供了相关功能。
在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。参考:Notification
Notification与浏览器中的事件冒泡比较相似,可以在上层节点的任意位置监听通知,也可以阻止冒泡,不再继续向上通知。
实现通知需要两个步骤。第一步是定义自定义的Notification
class MyNotification extends Notification {
MyNotification(this.msg);
final String msg;
}
然后需要使用NotificationListener
注册onNotification
回调,最后在NotificationListener
的后代组件中通过Notification实例的dispatch(context)
方法触发通知
class NotificationRoute extends StatefulWidget {
@override
NotificationRouteState createState() {
return new NotificationRouteState();
}
}
class NotificationRouteState extends State<NotificationRoute> {
String _msg="";
@override
Widget build(BuildContext context) {
//监听通知
return NotificationListener<MyNotification>(
onNotification: (notification) {
setState(() {
_msg+=notification.msg+" ";
});
return true;
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// 这种写法无法触发外层的onNotification,因为使用的context是NotificationRouteState的buildContext,
// 而不是NotificationListener后代组件的buildContext
// RaisedButton(
// onPressed: () => MyNotification("Hi").dispatch(context),
// child: Text("Send Notification"),
// ),
Builder(
builder: (context) {
// 需要使用builder获取NotificationListener后代组件的buildContext
return RaisedButton(
// 通过Notification实例的dispatch方法触发通知
onPressed: () => MyNotification("Hi").dispatch(context),
child: Text("Send Notification"),
);
},
),
Text(_msg)
],
),
),
);
}
}
这样,当dispatch(context)
时,父组件的NotificationListener<MyNotification>
就会收到对应的通知并执行onNotification
回调了。
NotificationListener
可以嵌套监听,并且当onNotification
方法返回true
时会阻止通知继续冒泡
NotificationListener<MyNotification>(
onNotification: (notification) {
setState(() {
_msg += notification.msg + " ";
});
return true;
},
child: NotificationListener<MyNotification>(
onNotification: (notification) {
print('stop notification');
return true; // 返回true阻止冒泡,外层的NotificationListener将无法收到通知,也就不会执行onNotification了
},
child: Builder(
builder: (context) {
return RaisedButton(
//按钮点击时分发通知
onPressed: () => MyNotification("Hi").dispatch(context),
child: Text("Send Notification"),
);
},
),
),
)
小结
本文主要总结了Flutter中InheritedWidget
与Notification
的基本用法,了解了在Flutter中
- 通过
InheritedWidget
向下与后代组件共享数据 - 了解使用
Provider
这种轻便的数据状态管理库 - 通过
Notification
向上通知祖先组件NotificationListener
监听事件
发现Flutter里面有很多思想跟Web是互通的,另外Flutter的源码并不如想象中的晦涩难懂,有机会的话应该从上而下整体过一遍,从顶层设计再深入某些功能的具体实现。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。