简介

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进程和改写的流程,见下图。

数据同享InheritedWidget运用与原理

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));
      }),
    ])),

当点击改写按钮的时分,增加几行输出信息,操控台会有如下输出

数据同享InheritedWidget运用与原理

看到操控台输出成果后,会有下面三个疑问:

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 办法上,仓库信息如下图

数据同享InheritedWidget运用与原理
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中,下一次改写的时分会改写这个控件。仓库信息如下:

数据同享InheritedWidget运用与原理
问题 3,为什么增加按钮没有改写? 已然都获取了ChangeNotifierProvider1,为什么增加按钮没有改写呢,仍是要回到2.2 部分 关于didChangeDependencies办法的介绍中,因为getElementForInheritedWidgetOfExactType办法不会注册InheritedElement 和当时 Element 的依靠联系,导致InheritedElement在notifyClients办法中没有调用增加按钮对应的 Element 的didChangeDependencies办法,该办法中的markNeedsBuild也没有调用,终究没有把当时 element 增加到 buildOwner 的_dirtyElements中,一起因为问题 1 中的说到的缓存机制,所以该控件没有改写。

2.3.3 element 缓存

上文的 updateChild 办法中,有两个办法,update 和 inflateWidget,他们的差异如下:

  1. update()

    • 当调用update()办法时,Flutter会查看当时Element的类型是否与新widget的类型匹配。
    • 假如类型匹配,则Flutter会测验重用现有的Element实例,以防止不必要的重建。
    • 经过重用现有Element实例,能够保存该元素的状况信息和相关上下文,以提高性能和效率
    • 这种缓存机制确保了在更新相同类型的widget时,不会呈现不及时的情况。
  2. inflateWidget()

    • 当调用inflateWidget()办法时,Flutter会创立一个新的Element实例,并将其增加到Element树中。
    • 新创立的Element实例不会重用现有的元素,而是完全新建一个。
    • 这意味着即便在更新相同类型的widget时调用inflateWidget()办法,也会创立一个新的Element实例,并在树中刺进它,而不是重用之前的Element
    • 这可能会导致一些额外的开支,尤其是在重复调用inflateWidget()时。

因而,在更新同一类型的widget时,运用update()办法,以确保更有效地运用缓存机制,防止不必要的重建。只有当需要在树中刺进一个新的widget时,才运用inflateWidget()办法。