【Flutter学习】之Widget数据共享之InheritedWidget
一,概述
业务开发中经常会碰到这样的情况,多个Widget需要同步同一份全局数据,比如点赞数、评论数、夜间模式等等。在安卓中,一般的实现方式是观察者模式,需要开发者自行实现并维护观察者的列表。在flutter中,原生提供了用于Widget间共享数据的InheritedWidget,当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。
InheritedWidget是一个特殊的Widget,开发者可以将其作为另一个子树的父级放在Widgets树中。该子树的所有小部件都必须能够与该InheritedWidget公开的数据进行交互。
二,源码分析
InheritedWidget
先来看下InheritedWidget的源码:abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child); @override
InheritedElement createElement() => new InheritedElement(this); @protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}它继承自ProxyWidget:
abstract class ProxyWidget extends Widget {
const ProxyWidget({ Key key, @required this.child }) : super(key: key);
final Widget child;
}可以看出Widget内除了实现了
createElement
方法外没有其他操作了,它的实现关键一定就是InheritedElement
了。
InheritedElement
来看下InheritedElement源码class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget); @override
InheritedWidget get widget => super.widget; // 这个Set记录了所有依赖的Element
final Set<Element> _dependents = new HashSet<Element>(); // 该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1)
@override
void _updateInheritance() {
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = new HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
} // 该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新
@override
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
for (Element dependent in _dependents) {
dependent.didChangeDependencies();
}
}
}其中
_updateInheritance
方法在基类Element中的实现如下:void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:
- InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
- 如果某节点的父节点有不止一个同一类型的InheritedWidget,调用
inheritFromWidgetOfExactType
获取到的是离自身最近的该类型的InheritedWidget。
- 看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到
_dependents
这个列表中的呢?回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过
inheritFromWidgetOfExactType
找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
// 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
// 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
// 这里将自己添加到了_dependents列表中,相当于注册了监听
ancestor._dependents.add(this);
return ancestor.widget;
}
_hadUnsatisfiedDependencies = true;
return null;
}
三,示例demo
- 自定义继承于InheritedWidget的类
//模型数据
class InheritedTestModel {
final int count;
const InheritedTestModel(this.count);
} //自定义MyInheritedWidget(可以把它看成古代边城的哨所)
class MyInheritedWidget extends InheritedWidget {
//构造方法
MyInheritedWidget({
Key key,
@required this.data,
@required Widget child //子组件
}):super(key:key,child:child); //属性
final InheritedTestModel data;//共享的数据model //类方法
static MyInheritedWidget of(BuildContext context){
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
} //示例方法
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
// TODO: implement updateShouldNotify
return data != oldWidget.data;
}
}代码解析:
此代码定义了一个名为“MyInheritedWidget”的Widget,旨在“共享”所有小部件(与子树的一部分)中的某些数据(data)。
如前所述,为了能够传播/共享一些数据,需要将InheritedWidget定位在窗口小部件树的顶部,这解释了传递给InheritedWidget基础构造函数的“@required Widget child”。
“Static MyInheritedWidget(BuildContext context)”方法允许所有子窗口小部件获取最接近上下文的MyInheritedWidget的实例(参见后面)
最后,“updateShouldNotify”重写方法用于告诉InheritedWidget是否必须将通知传递给所有子窗口小部件(已注册/已订阅),如果对数据应用了修改(请参阅下文)。
因此,我们需要将它放在树节点级别,如下所示:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: new Row(
children: <Widget>[
...
],
),
);
}
}
子child如何访问InheritedWidget的数据?
在构建子child时,后者将获得对InheritedWidget的引用,如下所示:class MyChildWidget... {
... @override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context); /// 从此刻开始,窗口小部件可以使用MyInheritedWidget公开的数据
/// 通过调用:inheritedWidget.data
return new Container(
color: inheritedWidget.data.color,
);
}
}
请考虑以下显示窗口小部件树结构的图表。
为了说明一种交互方式,我们假设如下:
- '小部件A’是一个将项目添加到购物车的按钮;
- 小部件B”是一个显示购物车中商品数量的文本;
- “小部件C”位于小部件B旁边,是一个内部带有任何文本的文本;
- 我们希望“Widget B”在按下“Widget A”时自动在购物车中显示正确数量的项目,但我们不希望重建“Widget C”
InheritedWidget就是用来干这个的Widget!
- 示例代码:
class Item {
String reference;
Item(this.reference);
} class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child); final MyInheritedWidgetState data; @override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
} class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key); final Widget child; @override
MyInheritedWidgetState createState() => new MyInheritedWidgetState(); static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
} class MyInheritedWidgetState extends State<MyInheritedWidget>{
List<Item> _items = <Item>[];
int get itemsCount => _items.length;
void addItem(String reference){
setState((){
_items.add(new Item(reference));
});
} @override
Widget build(BuildContext context){
return new _MyInherited(
data: this,
child: widget.child,
);
}
} class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
} class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
new WidgetC(),
],
),
),
],
),
),
);
}
} class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
} class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}');
}
} class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C');
}
} - 说明
在这个非常基本的例子中,
- 示例代码:
- _MyInherited是一个InheritedWidget,每次我们通过点击“Widget A”按钮添加一个Item时都会重新创建它
- MyInheritedWidget是一个Widget,其状态包含Items列表。可以通过“(BuildContext context)的静态MyInheritedWidgetState”访问此状态。
- MyInheritedWidgetState公开一个getter(itemsCount)和一个方法(addItem),以便它们可以被小部件使用,这是子小部件树的一部分
- 每次我们向State添加一个Item时,MyInheritedWidgetState都会重建
- MyTree类只是构建一个小部件树,将MyInheritedWidget作为树的父级
- WidgetA是一个简单的RaisedButton,当按下它时,从最近的MyInheritedWidget调用addItem方法
- WidgetB是一个简单的文本,显示最接近的MyInheritedWidget级别的项目数
- 这一切如何运作?
注册Widget以供以后通知
当子Widget调用MyInheritedWidget.of(context)时,它会调用MyInheritedWidget的以下方法,并传递自己的BuildContext。static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}在内部,除了简单地返回MyInheritedWidgetState的实例之外,它还将消费者窗口小部件订阅到更改通知。
在场景后面,对这个静态方法的简单调用实际上做了两件事:- 当对InheritedWidget应用修改时,“consumer”窗口小部件会自动添加到将重建的订户列表中(此处为_MyInherited)
- _MyInherited小部件(又名MyInheritedWidgetState)中引用的数据将返回给“使用者”
- 过程
由于’Widget A’和’Widget B’都已使用InheritedWidget订阅,因此如果对_MyInherited应用了修改,则当单击Widget A的RaisedButton时,操作流程如下(简化版本):- 调用MyInheritedWidgetState的addItem方法
- MyInheritedWidgetState.addItem方法将新项添加到List
- 调用setState()以重建MyInheritedWidget
- 使用List 的新内容创建_MyInherited的新实例
- _MyInherited记录在参数(数据)中传递的新State作为InheritedWidget,它检查是否需要“通知”“使用者”(答案为是)
- 它迭代整个消费者列表(这里是Widget A和Widget B)并请求他们重建
- 由于Wiget C不是消费者,因此不会重建。
但是,Widget A和Widget B都重建了,而重建Wiget A没用,因为它没有任何改变。如何防止这种情况发生?
在仍然访问“继承的”小组件时阻止某些小组件重建
Widget A也被重建的原因来自它访问MyInheritedWidgetState的方式。
正如我们之前看到的,调用context.inheritFromWidgetOfExactType()
方法的实际上是自动将Widget订阅到“使用者”列表。
防止此自动订阅同时仍允许Widget A访问MyInheritedWidgetState的解决方案是更改MyInheritedWidget的静态方法,如下所示:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
通过添加布尔类型的额外参数…
- 如果“rebuild”参数为true(默认情况下),我们使用常规方法(并且Widget将添加到订阅者列表中)
- 如果“rebuild”参数为false,我们仍然可以访问数据,但不使用InheritedWidget的内部实现
因此,要完成解决方案,我们还需要稍微更新Widget A的代码,如下所示(我们添加false额外参数):
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}在那里,当我们按下它时,Widget A不再重建。
【Flutter学习】之Widget数据共享之InheritedWidget的更多相关文章
- Flutter学习笔记(27)--数据共享(InheritedWidget)
如需转载,请注明出处:Flutter学习笔记(27)--数据共享(InheritedWidget) InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在widg ...
- Flutter学习笔记(9)--组件Widget
如需转载,请注明出处:Flutter学习笔记(9)--组件Widget 在Flutter中,所有的显示都是Widget,Widget是一切的基础,我们可以通过修改数据,再用setState设置数据(调 ...
- Flutter学习笔记(23)--多个子元素的布局Widget(Rwo、Column、Stack、IndexedStack、Table、Wrap)
如需转载,请注明出处:Flutter学习笔记(23)--多个子元素的布局Widget(Rwo.Column.Stack.IndexedStack.Table.Wrap) 上一篇梳理了拥有单个子元素布局 ...
- Flutter学习笔记(22)--单个子元素的布局Widget(Container、Padding、Center、Align、FittedBox、Offstage、LimitedBox、OverflowBox、SizedBox)
如需转载,请注明出处:Flutter学习笔记(22)--单个子元素的布局Widget(Container.Padding.Center.Align.FittedBox.Offstage.Limited ...
- Android程序员的Flutter学习笔记
作为忠实与较资深的Android汪, 最近抽出了一些时间研究了一下Google的亲儿子Flutter, 尚属皮毛, 只能算是个简单的记录吧. Google自2017年第一次提出Flutter, 到20 ...
- Flutter学习笔记(8)--Dart面向对象
如需转载,请注明出处:Flutter学习笔记(7)--Dart异常处理 Dart作为高级语言,支持面向对象的很多特性,并且支持基于mixin的继承方式,基于mixin的继承方式是指:一个类可以继承自多 ...
- Flutter学习笔记(10)--容器组件、图片组件
如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 上一篇Flutter学习笔记(9)--组件Widget我们说到了在Flutter中一个非常重要的理念"一切皆为组件 ...
- Flutter学习笔记(11)--文本组件、图标及按钮组件
如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 文本组件 文本组件(text)负责显示文本和定义显示样式,下表为text常见属性 Text组件属性及描述 属性名 类型 默认 ...
- Flutter学习笔记(12)--列表组件
如需转载,请注明出处:Flutter学习笔记(12)--列表组件 在日常的产品项目需求中,经常会有列表展示类的需求,在Android中常用的做法是收集数据源,然后创建列表适配器Adapter,将数据源 ...
随机推荐
- CGContextRef 使用小记
. 用CGContextRef 画文字 在 UIView的 - (void)drawRect:(CGRect)rect {} 方法中进行 CGContextRef context = UIGraphi ...
- 【Java】Java引用maven私服jar包及jar包提交私服问题
pom.xml中加入以下配置即可 1.引用私服jar包 <!-- 加载的是 第三方项目使用的jar包 --> <repositories> <repository> ...
- 转载:IDEA lombok插件的安装和使用
转载自:https://jingyan.baidu.com/article/0a52e3f4e53ca1bf63ed725c.html lombok插件的安装 1 首先我们需要安装IntelliJ ...
- 杂项:JFB-权限设置
ylbtech-杂项:JFB-权限设置 1. 家政经理返回顶部 1. if (UserContext.GetTeamId() == (int)UserType.Manager) { condition ...
- Oracle -操作数据库
删除数据: delete:用delete删除记录,Oracle系统会产生回滚记录,所以这种操作可以使用ROLLBACK来撤销 truncate:删除数据时,不会产生回滚记录.所以执行速度相对较快些 可 ...
- Java 反射简介(转载)
反射机制是什么 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为java ...
- python网络爬虫实战之快速入门
本系列从零开始阐述如何编写Python网络爬虫,以及网络爬虫中容易遇到的问题,比如具有反爬,加密的网站,还有爬虫拿不到数据,以及登录验证等问题,会伴随大量网站的爬虫实战来进行. 我们编写网络爬虫最主要 ...
- 安装vue开发环境→安装淘宝镜像的时候报错
问题: npm WARN deprecated socks@1.1.10: If using 2.x branch, please upgrade to at least 2.1.6 to avoid ...
- JPA、Hibernate、Spring Data JPA 的关系,你懂吗?
来源:https://my.oschina.net/u/3080373/blog/1828589 什么是JPA? 全称Java Persistence API,可以通过注解或者XML描述[对象-关系表 ...
- 牛客网挑战赛24 青蛙(BFS)
链接:https://www.nowcoder.com/acm/contest/157/E来源:牛客网 有一只可爱的老青蛙,在路的另一端发现了一个黑的东西,想过去一探究竟.于是便开始踏上了旅途 一直这 ...