简介
InheritedWidget
是 Flutter 中非常重要的一个功能型组件,它供给了一种在 widget 树中从上到下同享数据的方式。
一、运用
先写一个最简单的比如来看看怎么怎么运用,创立InheritedWidget的子类
class ShareDataWidget extends InheritedWidget{
ShareDataWidget({Key? key,required this.data,required Widget child}) : super(key: key,child: child);
final int data;
static ShareDataWidget? of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
// return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget;
}
@override
bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
// TODO: implement updateShouldNotify
return oldWidget.data != data;
}
}
新增一个控件,运用ShareDataWidget的内容
class TestWidgetState extends State<TestWidget>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Text(ShareDataWidget.of(context)!.data.toString());
}
}
运用这个控件来到达内容的改变。
ShareDataWidget(data: _counter, child: TestWidget()),
GestureDetector(onTap: (){
setState(() {
_counter ++;
});
},)
该示例仅仅为了展示InheritedWidget的运用,实践情况下想要完成这个效果能够不必运用InheritedWidget,别的这样的代码存在缺点,一个缺点是需要手动调用改写,别的假如 TestWidget 里面存在多个控件,有的控件依靠了ShareDataWidget的数据,有的控件不依靠,但是当点击按钮时,一切的控件都改写了,后文会解析原因而且优化。
二、原理篇
2.1 怎么存储与获取
2.1.1 存储位置
abstract class Element extends DiagnosticableTree implements BuildContext {
……
PersistentHashMap<Type, InheritedElement>? _inheritedWidgets;//这儿存储的是InheritedElement目标,会在取的时分获取对应的 Widget 目标
……
}
每一个InheritedWidget的子 Widget 控件对应的 Element 中都存有一切的父InheritedWidget目标。
2.1.2 存储进程
Element 存储_inheritedWidgets的时机是在 mount 办法中进行的
void mount(Element? parent, Object? newSlot) {
……
_updateInheritance();
……
}
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;//把父控件的_inheritedWidgets继承过来
}
InheritedElement重写了_updateInheritance办法,除了会把父控件的_inheritedWidgets继承过来,一起也会把自己增加到_inheritedWidgets中去:
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final PersistentHashMap<Type, InheritedElement> incomingWidgets =
_parent?._inheritedWidgets ?? const PersistentHashMap<Type, InheritedElement>.empty();
_inheritedWidgets = incomingWidgets.put(widget.runtimeType, this);
}
mount办法一层一层调用结束后,就会把InheritedElement一级一级的赋值给一切的 element 目标。
2.1.3 获取
获取InheritedWidget
的办法有两种
context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()
context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget
这两个办法都是从_inheritedWidgets
依据泛型获取对应是InheritedElement
目标,然后获取 element 对应的 widget 目标,这样就能够做到数据同享了。
例如dependOnInheritedWidgetOfExactType代码如下:
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
2.2 注册依靠联系和关于didChangeDependencies
办法
上面两个获取InheritedWidget
的办法有一个差异,一个dependOnInheritedWidgetOfExactType会触发didChangeDependencies
办法,而别的一个不会,来看一下原因。
官方关于这个办法的解说是这样的:
/// Called when a dependency of this [State] object changes. /// /// For example, if the previous call to [build] referenced an /// [InheritedWidget] that later changed, the framework would call this /// method to notify this object about the change. /// /// This method is also called immediately after [initState]. It is safe to /// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method. /// /// Subclasses rarely override this method because the framework always /// calls [build] after a dependency changes. Some subclasses do override /// this method because they need to do some expensive work (e.g., network /// fetches) when their dependencies change, and that work would be too /// expensive to do for every build.
里面说到,在initState办法后会直接调用,或者InheritedWidget发生改动后。直接调用是在element 的_firstBuild
办法中进行的,该办法中会调用 state 的initState
办法。然后调用didChangeDependencies
办法,那InheritedWidget中又是怎么触发didChangeDependencies的呢。
Element 中是依据_didChangeDependencies变量来操控performRebuild
办法中是否要调用 state 的didChangeDependencies办法的,具体见performRebuild办法
上文说到,运用dependOnInheritedWidgetOfExactType办法获取 InheritedWidget 目标时,当内容发生改变,会触发 State 的didChangeDependencies办法,而别的一个不会,看来秘密在dependOnInheritedWidgetOfExactType办法中。运用dependOnInheritedWidgetOfExactType办法获取能够到达内容改变时调用didChangeDependencies办法,做了下面几件工作。
2.2.1 注册依靠联系
修正注册联系,在dependOnInheritedWidgetOfExactType办法中,除了从_inheritedWidges中获取InheritedWidget,还注册了父控件的依靠联系,存储在element(InheritedWidget的子控件的element)的Set<InheritedElement>? _dependencies;
中,代码如下:
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;//注册依靠联系
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);//这儿是子控件来注册对父控件的依靠
ancestor.updateDependencies(this, aspect);//这个办法用来在父控件中注册子控件的依靠,ancestor是父控件
return ancestor.widget as InheritedWidget;
}
一起在InheritedElement的final Map<Element, Object?> _dependents
中也注册了子控件的依靠,经过updateDependencies办法完成。
2.2.2 依靠联系 的存储位置
class InheritedElement extends ProxyElement {
/// Creates an element that uses the given widget as its configuration.
InheritedElement(InheritedWidget super.widget);
/// 父 InheritedElement 中存储一切依靠了它的子 element
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
...
}
abstract class Element extends DiagnosticableTree implements BuildContext {
...
Set<InheritedElement>? _dependencies;//子控件 element 中存有一切它依靠的 InheritedElement
...
}
2.2.3 修正_didChangeDependencies变量
第一步注册了依靠联系后,在这儿就用到了,是否需要修正子控件element的_didChangeDependencies的变量值为true,取决于是否注册了依靠联系。在InheritedElement的update办法中,终究会调用notifyClients
办法,在该办法中会遍历一切被注册了依靠联系的子控件(存在_dependents中),分别调用notifyDependent
办法,在该办法中,会修正StatefulElement的中_didChangeDependencies
属性,将它标记为true。
2.2.4 依据_didChangeDependencies判断是否调用
StatefulElement在调用performRebuild
办法时,当_didChangeDependencies是true时,会调用state的didChangeDependencies办法,代码如下:
void performRebuild() {
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
同理,因为getElementForInheritedWidgetOfExactType办法没有调用dependOnInheritedElement办法,则不会增加依靠联系,自然不会触发后面的过程了,当改写的时分,因为_didChangeDependencies是false,则state的didChangeDependencies办法不会调用。
修正_didChangeDependencies进程和改写的流程,见下图。
2.3 Provider的改写问题
2.3.1 现象
Provider 库运用,有如下代码:
ChangeNotifierProvider<CartModel>(
create: (_) => CartModel(),
child: Row(children: [
Builder(builder: (context) {
print("1、改写其他控件");
return Text("其他控件");
}),
Consumer<CartModel>(builder: (context, model, wiget) {
print("2、改写总数");
return Text("总数:" + model.total.toString());
}),
Builder(builder: (context) {
print("3、改写增加按钮");
return TextButton(
onPressed: () {
Provider.of<CartModel>(context,listen: false).add(Item(2, 4));
},
child: const Icon(Icons.add));
}),
])),
当点击改写按钮的时分,增加几行输出信息,操控台会有如下输出
看到操控台输出成果后,会有下面三个疑问:
1、其他控件为什么没有从头构建?
2、总数为什么能够改写?
3、增加按钮为什么没有改写?
2.3.2 原因
问题一,其他控件为什么没有从头构建?
先模仿 Provider库 完成一个简易版本的 ChangeNotifierProvider,便利剖析问题
class ChangeNotifierProvider1<T extends ChangeNotifier> extends StatefulWidget{
ChangeNotifierProvider1({Key? key, required this.child, required this.data});
final Widget child;
final T data;
static T of<T>(BuildContext context,{bool listen = true}){
final provider = listen == true ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>() : context.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()?.widget
as InheritedProvider<T>;
return provider!.data;
}
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return ChangeNotifierProviderState1<T>();
}
@override
StatefulElement createElement() {
// TODO: implement createElement
return ChangeNotifierProviderElement(this);
}
}
class ChangeNotifierProviderElement extends StatefulElement{
ChangeNotifierProviderElement(super.widget);
@override
void update(covariant StatefulWidget newWidget) {
// TODO: implement update
super.update(newWidget);
}
}
class ChangeNotifierProviderState1<T extends ChangeNotifier> extends State<ChangeNotifierProvider1<T>>{
@override
Widget build(BuildContext context) {
// TODO: implement build
print("调用ChangeNotifierProviderState的 build 办法");
return InheritedProvider<T>(child: widget.child, data: widget.data);
}
@override
void didUpdateWidget(ChangeNotifierProvider1<T> oldWidget){
if(oldWidget != widget.child){
widget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
// TODO: implement initState
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
// TODO: implement dispose
widget.data.removeListener(update);
super.dispose();
}
当 widget.data 数据改变时会调用ChangeNotifierProviderState1的 update 办法,会从头调用ChangeNotifierProviderElement的 rebuild 办法,终究调用到InheritedProviderElementd的 updateChild 办法上,仓库信息如下图
updateChild 是 父类element的办法,它的核心代码如下:
/// Update the given child with the given new configuration.
///
/// This method is the core of the widgets system. It is called each time we
/// are to add, update, or remove a child based on an updated configuration.
///
/// The `newSlot` argument specifies the new value for this element's [slot].
///
/// If the `child` is null, and the `newWidget` is not null, then we have a new
/// child for which we need to create an [Element], configured with `newWidget`.
///
/// If the `newWidget` is null, and the `child` is not null, then we need to
/// remove it because it no longer has a configuration.
///
/// If neither are null, then we need to update the `child`'s configuration to
/// be the new configuration given by `newWidget`. If `newWidget` can be given
/// to the existing child (as determined by [Widget.canUpdate]), then it is so
/// given. Otherwise, the old child needs to be disposed and a new child
/// created for the new configuration.
///
/// If both are null, then we don't have a child and won't have a child, so we
/// do nothing.
///
/// The [updateChild] method returns the new child, if it had to create one,
/// or the child that was passed in, if it just had to update the child, or
/// null, if it removed the child and did not replace it.
///
/// The following table summarizes the above:
///
/// | | **newWidget == null** | **newWidget != null** |
/// | :-----------------: | :--------------------- | :---------------------- |
/// | **child == null** | Returns null. | Returns new [Element]. |
/// | **child != null** | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
///
/// The `newSlot` argument is used only if `newWidget` is not null. If `child`
/// is null (or if the old child cannot be updated), then the `newSlot` is
/// given to the new [Element] that is created for the child, via
/// [inflateWidget]. If `child` is not null (and the old child _can_ be
/// updated), then the `newSlot` is given to [updateSlotForChild] to update
/// its slot, in case it has moved around since it was last built.
///
/// See the [RenderObjectElement] documentation for more information on slots.
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (child != null) {
bool hasSameSuperclass = true;
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) {
//注释 1:假如 child 的 widget 和 newWidget 相同,则不改写 child
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
...
//注释 2:更新child 关联的 widget
child.update(newWidget);
...
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
//注释 3: 基于 newWidget创立新的 element
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//注释 4: 基于 newWidget创立新的 element
newChild = inflateWidget(newWidget, newSlot);
}
}
从上述注释1的当地能够看出,只要不改写ChangeNotifierProvider1的父 Widget,不论怎么改写ChangeNotifierProviderState1
内部的时分,他 对应Widget的child 没有变过(从构造函数传入的,也是这段代码里的newWidget),所以他的 child 不会改写,所以 Row 控件不会改写,他的子控件其他控件
也不会改写。
问题 2,已然 Row 不会改写,那为什么总数控件能够改写?
因为总数控件运用dependOnInheritedWidgetOfExactType
获取ChangeNotifierProvider1
,而且获取它存储的 data, 从 2.2 的关于 dependencies 的介绍的流程图里能够看出,当InheritedWidget 更新的时分,终究会调用 child 的markNeedsBuild
办法,该办法会把当时 element 增加到 buildOwner 的_dirtyElements
中,下一次改写的时分会改写这个控件。仓库信息如下:
问题 3,为什么增加按钮没有改写?
已然都获取了ChangeNotifierProvider1,为什么增加按钮没有改写呢,仍是要回到2.2 部分 关于didChangeDependencies
办法的介绍中,因为getElementForInheritedWidgetOfExactType
办法不会注册InheritedElement 和当时 Element 的依靠联系,导致InheritedElement在notifyClients
办法中没有调用增加按钮对应的 Element 的didChangeDependencies
办法,该办法中的markNeedsBuild
也没有调用,终究没有把当时 element 增加到 buildOwner 的_dirtyElements中,一起因为问题 1 中的说到的缓存机制,所以该控件没有改写。
2.3.3 element 缓存
上文的 updateChild 办法中,有两个办法,update 和 inflateWidget,他们的差异如下:
-
update()
:- 当调用
update()
办法时,Flutter会查看当时Element
的类型是否与新widget的类型匹配。 - 假如类型匹配,则Flutter会测验重用现有的
Element
实例,以防止不必要的重建。 - 经过重用现有
Element
实例,能够保存该元素的状况信息和相关上下文,以提高性能和效率。 - 这种缓存机制确保了在更新相同类型的widget时,不会呈现不及时的情况。
- 当调用
-
inflateWidget()
:- 当调用
inflateWidget()
办法时,Flutter会创立一个新的Element
实例,并将其增加到Element
树中。 - 新创立的
Element
实例不会重用现有的元素,而是完全新建一个。 - 这意味着即便在更新相同类型的widget时调用
inflateWidget()
办法,也会创立一个新的Element
实例,并在树中刺进它,而不是重用之前的Element
。 - 这可能会导致一些额外的开支,尤其是在重复调用
inflateWidget()
时。
- 当调用
因而,在更新同一类型的widget时,运用update()
办法,以确保更有效地运用缓存机制,防止不必要的重建。只有当需要在树中刺进一个新的widget时,才运用inflateWidget()
办法。