【Flutter脱发实录】盘一盘InheritedWidget

·  阅读 366

InheritedWidget简介

Widget篇中,讲述了StatefulWidget如何管理自身的状态。但是开发一款App经常会出现多个页面数据共享的场景。于是Flutter提供了一个功能型的组件来解决数据传递的问题——InheritedWidget


数据获取

要理解InheritedWidget的如何传递数据的,要先解析其实现思路,然后带着这个思路去看源码,这样就会清晰很多。

实现思路

通常情况下,某个模块内的数据共享,全局数据共享,数据都是向下传递的。由于Flutter中整个UI架构是由Element Tree支撑的树状结构,并且只对我们暴露Widget树,那么我们可以把数据可以存放在某个Element对象持有的Widget上,并通过某个特定的方式,让子叶节点可以拿到这个Element对象,从而间接拿到Element对象上存储的数据。

Flutter是正通过如下步骤,实现上述思路的:

  1. 继承InheritedWidget并设置数据 为了区分Element是否携带数据,Flutter定义了一个特定的Element——InheritedElement,和其持有的配置文件InheritedWidget。 当我们在构建UI树,需要在某个节点存放数据时,我们可以继承InheritedWidget,并且定义一些数据data。对于Widget层,我们暂时只关心这些就够了,其他交给内部InheritedElement去处理。

  2. 生成一个映射表吗,并向下传递 思路中提到一个特定的方式,关于这个特定的方式,Flutter给出的方案是使用runtimeType作为key生成一张与Element的映射表,子节点根据这个key去查找对应的Element,获取数据。 我们开发业务时,数据通常是不同的,因此在第一步中创建的子InheritedWidget的也是不同的,可以使用此WidgetruntimeType作为映射表的key。 在每个Element生成时,会从父Element拷贝一份映射表,如果自己为数据节点InheritedElement,则把自己也添加进去。最后每个Element都会持有一份包含所有携带数据的InheritedElement的“目录”,层级越深的节点,“目录”信息也就越多。

  3. 查询映射表,获取数据 在每个节点,我们要获取上层数据时,只需要传入数据节点的runtimeType就可以拿到数据了。

原理理解了,进入源码世界一探究竟。

源码解析

直接看下关键的ElementInheritedElement类:

/// 通用Element获取数据节点
abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 存储的映射表
  Map<Type, InheritedElement> _inheritedWidgets;

  /// 加载时会复制parent的映射表
  void mount(Element parent, dynamic newSlot) {
    _updateInheritance();
  }

  void _updateInheritance() {
    _inheritedWidgets = _parent?._inheritedWidgets;
  }

  /// 根据InheritedWidget子类的泛型,查找对应的Widget
  /// @override 重写的是BuildContext接口中定义的方法 可以通过上下文context调用
  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
      {Object aspect}) {
    /// 根据Type查找映射表
    final InheritedElement ancestor =
        _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    return null;
  }

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    ...
    return ancestor.widget;
  }
}
复制代码
/// 数据节点获取数据及注册映射表
class InheritedElement extends ProxyElement {

  /// 获取Widget以获取Widget.data
  @override
  InheritedWidget get widget => super.widget as InheritedWidget;

  /// 更新InheritedWidget维护的InheritedElement映射表
  /// 当从父类获取到的表为null时,即自己为根节点时,创建新的表,并把自己添加进去
  /// 当从父类获取到的表不为null时,从父类复制一张表,并把自己添加进去,key为widget.runtimeType
  @override
  void _updateInheritance() {
    final Map<Type, InheritedElement> incomingWidgets =
        _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets[widget.runtimeType] = this;
  }
}
复制代码

小结Demo

写个简单的Demo,总结一下获取数据的流程:

class DemoInheritedWidget extends InheritedWidget {
  /// 自定义需要传递的数据
  int data;

  Widget child;

  DemoInheritedWidget({this.data, this.child});

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return true;
  }
}

class InheritedWidgetDemo extends StatefulWidget {
  @override
  _InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}

class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('DemoInheritedWidget')),
      body: Center(
        /// 嵌入数据节点并初始化数据值
        child: DemoInheritedWidget(
          data: 23333,
          child: Builder(
            /// 通过context上下文指定DemoInheritedWidget类型获取Widget对象和data数据
            builder: (context) => Text(
                '从上层获取的数据:${context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>().data}'),
          ),
        ),
      ),
    );
  }
}
复制代码

获取InheritedWidget数据


相关依赖响应

从上层数据节点中获取数据源的原理已经了解,当数据发生变化时,Flutter又是如何通知相关Element的呢?不会是通知所有子节点吧?

实现思路

当数据发生变更时,我们只想让那些使用这些数据的Widget响应就可以了。第一反应就是,把这些Widget都记录下来,然后只通知这些Widget响应不就解决了么?是的,Flutter也是这么想的。

  1. 记录依赖关系 Widget家族中,实际掌权的是Element,每次获取数据,都是由Element对象去操办的。于是我们可以在数据节点InheritedElement中,定义一个依赖表,当每次一个Element使用到此节点时,将其加入依赖表。

  2. 子节点选择性响应 当数据节点发生变化时,仅通知与依赖于它的节点进行响应。并且当数据未发生变化时,不通知子节点。

源码解析

思路很简单,来看下源码里是怎么实现的: 初看源码时,会注意到一个didChangeDependencies方法,因为这个方法会根据依赖关系,选择性执行。可以理解为一个响应方法。 ==其实细看源码会发现,实际有两个didChangeDependencies方法,作用是不同的。包括网上很多文章都忽视了这一点,将它们混为一谈了。==

/// 通用Element类中的didChangeDependencies方法
abstract class Element extends DiagnosticableTree implements BuildContext {
  /// 发生依赖时进行的响应   默认为标记重构方法  即调用此方法将立即标记重构,不需要进行diff操作
  void didChangeDependencies() {
    /// 标记此节点需要重构  交由BuildOwner处理
    markNeedsBuild();
  }
}

/// State中的didChangeDependencies方法
abstract class State<T extends StatefulWidget> with Diagnosticable {
  /// State中自己的didChangeDependencies方法 与Element无关
  /// 默认空实现,目的是用来在接收到依赖变更通知响应时,从build方法中抽离出来一部分耗资源的操作,避免build方法卡顿
  @protected
  void didChangeDependencies() { }
}

/// StatefulElement中关联两个didChangeDependencies方法 
class StatefulElement extends ComponentElement {

  /// State是否需要执行didChangeDependencies
  bool _didChangeDependencies = false;
  /// 持有的State对象
  State<StatefulWidget> _state;
  
  /// 重写父类Element的方法,在重构之后,标记需要State执行didChangeDependencies操作
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
  }
  
  /// 在重构后,调用State的didChangeDependencies方法
  @override
  void performRebuild() {
    if (_didChangeDependencies) {
      _state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }
}
复制代码

如何做到只通知相关依赖的呢?同样用一张依赖表做记录。

/// 数据节点自己管理依赖表
class InheritedElement extends ProxyElement {
  /// 依赖表 暂时只关注key  不关注value
  final Map<Element, Object> _dependents = HashMap<Element, Object>();

  /// 查找依赖
  @protected
  Object getDependencies(Element dependent) {
    return _dependents[dependent];
  }

  /// 设置依赖
  @protected
  void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
  }

  /// 默认设置value为null的依赖关系  可被子类重写自定义aspect
  @protected
  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

  /// 通知依赖的Element进行依赖变更时的操作
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }

  /// 更新
  @override
  void updated(InheritedWidget oldWidget) {
    /// 根据Widget中定义的条件  判断是否需要通知子节点
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget); // => super => ProxyElement => notifyClients()
  }

  /// 通知依赖的逻辑  即上述方法的super.updated()
  @override
  void notifyClients(InheritedWidget oldWidget) {
    /// 遍历依赖表中的Element 依次调用didChangeDependencies()
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
}
复制代码

众所周知Widget是对外暴露配置信息,因此InheritedWidget中提供了一个抽象方法留给我们定义通知子节点响应的时机:

abstract class InheritedWidget extends ProxyWidget {
  /// 需子类重写 告知Element是否需要通知子节点
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
复制代码

小结Demo

假如要实现这么一个功能,记录一位程序员的发量,并根据发量全网查找适合的产品。

class DemoInheritedWidget extends InheritedWidget {
  /// 自定义需要传递的数据
  String coderName;

  int coderHair;

  Widget child;

  DemoInheritedWidget({this.coderName, this.coderHair, this.child});

  /// 重写更新条件  当数据不相同时  通知重构
  @override
  bool updateShouldNotify(DemoInheritedWidget oldWidget) {
    return coderHair != oldWidget.coderHair;
  }
}

class _TestWidget1State extends State<TestWidget1> {
  String name = '';
  int hair = 0;
  String recGoods = '';

  @override
  Widget build(BuildContext context) {
    print('患者信息build');
    DemoInheritedWidget data =
        context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>();
    name = data.coderName;
    hair = data.coderHair;
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('患者姓名:$name', style: TextStyle(fontSize: 20)),
        Text('当前发量:$hair', style: TextStyle(fontSize: 20)),
        Text('推荐产品:$recGoods', style: TextStyle(fontSize: 20))
      ],
    );
  }

  @override
  void didChangeDependencies() async {
    recGoods = await Future<String>.delayed(
        Duration(seconds: 3), () => '霸王防脱${(10000 - hair) ~/ 1000}号');
    print('推荐产品更新');

    /// 重构页面
    setState(() {});
    super.didChangeDependencies();
  }
}

  @override
  void didChangeDependencies() async {
    /// 模拟耗时操作 3秒出结果
    recGoods = await Future<String>.delayed(
        Duration(seconds: 3), () => '霸王防脱${(10000 - hair) ~/ 1000}号');

    /// 重构页面
    setState(() {});
    super.didChangeDependencies();
  }
}

class _TestWidget2State extends State<TestWidget2> {
  int salary = 2000;

  @override
  Widget build(BuildContext context) {
    print('工资build');
    return Text('当前工资:$salary¥',
        style: TextStyle(fontSize: 20, color: Colors.red));
  }

  @override
  void didChangeDependencies() {
    setState(() {
      salary += 1000;
    });
    print('加工资啦');
    super.didChangeDependencies();
  }
}

class InheritedWidgetDemo extends StatefulWidget {
  @override
  _InheritedWidgetDemoState createState() => _InheritedWidgetDemoState();
}

class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> {
  int num = 10000;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('DemoInheritedWidget')),
        body: Center(
            child: DemoInheritedWidget(
                coderName: '爱新觉罗狗剩儿',
                coderHair: num,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [TestWidget2(), TestWidget1()],
                ))),
        floatingActionButton: FloatingActionButton(
            child: Text('加班'),
            onPressed: () {
              setState(() {
                num -= 1000;
              });
            }));
  }
}
复制代码

运行结果如下图: Demo2

控制台日志如下:

I/flutter ( 4515): 加工资啦
I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build

I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build

I/flutter ( 4515): 工资build
I/flutter ( 4515): 患者信息build
I/flutter ( 4515): 推荐产品更新
I/flutter ( 4515): 患者信息build
复制代码

从运行结果和日志中,可以发现,如下几个结论:

  1. 初次加载时会出现“加工资啦” => firstBuild时,会调用didChangeDependencies方法。
  2. 后续“加班”只会减少发量不会加工资 => 只有使用context.dependOnInheritedWidgetOfExactType<DemoInheritedWidget>()获取数据间接注册依赖的State才会执行didChangeDependencies方法。
  3. 不管有没有依赖,Widgetbuild方法始终都是会执行的。难道也重构了?其实build方法只是执行了一段dart代码生成了一个新的Widget,也就是源码中的newWidget,只有在Widget.canUpdate返回true时,才会通知Element更新。具体的更新逻辑要看Widget Tree的变化,雨InheritedWidget无瓜。

有个坑

demo中的数据都是使用基本数据类型,如果采用对象将其封装起来,那么在updateShouldNotify方法中处理数据时,将会发现新老数据会是相同的。可能是因为引用类型变量,采用浅拷贝导致。


不发生依赖

从源码中可以看到,在使用dependOnInheritedWidgetOfExactType方法获取数据之后,会默认将自己添加到ancestor_dependencies依赖表中:

T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    /// 添加进依赖表
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
复制代码

那么如何做到万花丛中过,片叶不沾身,只获取数据不发生关系呢? 源码中提供了getElementForInheritedWidgetOfExactType方法获取InheritedElement,拿到了InheritedElement就可以拿到InheritedWidget和其数据啦!

InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    return ancestor;
  }
复制代码

这可以实现一些控制的操作,比如按钮之类的,只操作数据,而不发生Element重构。

InheritedModel

之前跳过了一个内容,就是在管理依赖表_dependents时,只用到了key值,没有用到value值。

final Map<Element, Object> _dependents = HashMap<Element, Object>();

void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }
复制代码

在源码中,使用aspect命名了这个value值,aspect即方面、片面的意思。也就是说,当前的Element只关注数据model的某个方面,换个角度来说,可以将相关依赖的子节点通过aspect进行分来,从而达成分类通知的功能。而InheritedModel就是对InheritedWidgetaspect使用上的一个封装。

源码解析

InheritedModel源码内容很少,所以直接分析源码:

abstract class InheritedModel<T> extends InheritedWidget {
  /// 表示当前节点是否属于某个方面aspect 需要由子类重写  默认为true
  bool isSupportedAspect(Object aspect) => true;

  /// 重写此方法,根据注册时指定的aspect 定义是否需要调用子节点的didChangeDependencies方法
  bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);
  
  /// 核心方法
  static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
    /// 没有指定aspect 则找到最近的一个数据节点
    if (aspect == null)
      return context.dependOnInheritedWidgetOfExactType<T>();

    /// 创建一个空的列表
    final List<InheritedElement> models = <InheritedElement>[];
    /// 向上递归查找 直到找到第一个支持aspect的节点或数据源T节点  将所有中间节点记录到列表中
    _findModels<T>(context, aspect, models);
    if (models.isEmpty) {
      return null;
    }

    /// 以下代码的作用是  获取到支持aspect 的数据节点 T  并且将与T之间所有节点都使用当前aspect注册依赖关系
    final InheritedElement lastModel = models.last;
    for (final InheritedElement model in models) {
      final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
      if (model == lastModel)
        return value;
    }
    return null;
  }

  /// 向上逐级递归查找符合条件InheritedElement
  static void _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect, List<InheritedElement> results) {
    final InheritedElement model = context.getElementForInheritedWidgetOfExactType<T>();
    /// 当前节点不是T的子节点时  跳出递归
    if (model == null)
      return;

    results.add(model);

    final T modelWidget = model.widget as T;
    /// 当查找到第一个  支持aspect 的节点时 跳出递归
    if (modelWidget.isSupportedAspect(aspect))
      return;

    Element modelParent;
    model.visitAncestorElements((Element ancestor) {
      modelParent = ancestor;
      return false;
    });
    if (modelParent == null)
      return;

    _findModels<T>(modelParent, aspect, results);
  }
}

class InheritedModelElement<T> extends InheritedElement {
  /// 使用aspect注册依赖
  @override
  void updateDependencies(Element dependent, Object aspect) {
    final Set<T> dependencies = getDependencies(dependent) as Set<T>;
    if (dependencies != null && dependencies.isEmpty)
      return;

    if (aspect == null) {
      setDependencies(dependent, HashSet<T>());
    } else {
      setDependencies(dependent, (dependencies ?? HashSet<T>())..add(aspect as T));
    }
  }
  
  /// 根据widget定义的更新条件决定是否执行dependent.didChangeDependencies()
  @override
  void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
    final Set<T> dependencies = getDependencies(dependent) as Set<T>;
    if (dependencies == null)
      return;
    if (dependencies.isEmpty || widget.updateShouldNotifyDependent(oldWidget, dependencies))
      dependent.didChangeDependencies();
  }
}
复制代码

小结

InheritedModel实际就是对InheritedWidgetupdateShouldNotify方法的一个拓展。重写updateShouldNotifyDependent方法,根据数据与aspect的关系,通知指定类别子节点做出响应。而子节点通过InheritedModel.inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect })方法注册片面依赖关系。


总结

InheritedWidget是Flutter中非常重要的一个功能型组件,但是我们通常不会直接使用他,而是对他进行一定程度的封装后再使用。

本文简单摸索了其实现原理,后续会继续学习与InheritedWidget相关的各种有意思的封装。

以上仅是自己阅读源码时的理解,若有错误之处,欢迎大家指出,一起探讨!

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改