跟🤡杰哥一起学Flutter (十三、从Widget源码 ✈ InheritedWidget)

本文为稀土技能社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!

1. 引言

前面写的《十一、Flutter UI结构聊》一文中说到 Flutter 的本质是一套 UI结构,处理的是 一套代码在多端烘托。Flutter 的架构从上到下分为如下 三层 ,上层组件依靠基层组件,组件间无法跨层拜访:

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

作为一个UI崽,大部分时间都是在 Framework堆Widget,个人感觉仍是有必要了解下Widget背面的原理哒。先回顾下Flutter中关于Widget中心设计理念的名词:

  • 万物皆Widget:不管是 结构(如按钮、文本)、布局 仍是 样式,只需 与图形构建相关 的UI元素,都是Widget,它只是 描绘UI元素配置信息的笼统
  • 声明式+响应式UI:无需经过 指令式代码 来手动创立和办理UI的各个部分,只需经过声明性代码来描绘界面的外观和结构。当运用程序的状况改动时,Flutter结构会主动重建UI,以匹配新的状况。
  • 不可变性:Widget一旦被创立,就不能被修改,每次状况改动时,Flutter会依据最新的状况创立一棵新的Widget树,这种不变性有助于进步运用程序的性能,使得Flutter能够快速确定哪些Widget需求在Element树和RenderObject树中更新,而不是每次都重建整个UI树。
  • 组合优于承继:鼓舞经过组合小的、简略的目标来创立杂乱的目标,而不是经过创立杂乱的承继结构。

然后是陈词滥调的烘托 “三棵树 “:

  • Widget对视图的结构化描绘,存储视图烘托相关的 配置信息:布局、烘托特点、事件响应等信息。
  • ElementWidget的实例化目标,承载视图构建的 上下文数据 (BuildContext) ,连接Widget到完结终究 烘托桥梁
  • RenderObject → 担任完成视图烘托的目标;

概念的东西回顾得差不多了,本节带着大伙来康康Widget的源码,为后边的 状况办理 章节做准备,内容如下:

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

Tips:本节触及的源码都是简化过的,只保存了关键代码以便利阅读,更详细代码可自行检查

2. Widget

2.1. 源码

// 不可变注解
@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key? key;
  @protected
  @factory
  Element createElement();
  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  //其它与调试相关的办法...
}

源码解读

  • 承继笼统类 DiagnosticableTree(诊断树) ,用于供给调试信息相关的办法;
  • 界说了一个 Key,看过前面写的《十、进阶-玩转各种Key》就知道它是 Widget的仅有标识,用于 比较Widget是否持平,以及在 Widget移动或改动时坚持其状况
  • createElement()将Widget实例化为一个详细的Element,Widget或许被包含在UI树中零次或屡次。每次被包含在UI树中,都会被实例化为一个Element,即Widget和Element的联系是 一对多
  • toStringShort():回来此Widget的简略文本描绘,一般是 runtimeType-key。
  • canUpdate() :用于 比较两个Widget是否持平,假如新旧Widget的runtimeType和key都 持平,那Flutter结构会以为它们是 相同 的,更新 现有的Element以反映新Widget,而不是创立一个新的Element。假如不持平,删去 旧Element,创立与新Widget对应的新Element,并将其刺进到UI树中的相应方位。

就一个Key,的确 轻量,不过实践开发中很少直接用Widget,而是用它的子类,先来看下它的承继联系吧~

2.2. 承继联系

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

图中能够看到Widget有4个直接子类,顺次看看源码~

3. StatelessWidget

没有状况改动,仅用做展示,UI表现形式取决于 结构函数中供给的参数

3.1. 源码

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key? key }) : super(key: key);
  @override
  Widget build(BuildContext context);
  @override
  StatelessElement createElement() => StatelessElement(this);
}

源码解读

  • 结构办法答应传入一个可选的Key,用于操控结构是否应该在Widget重建时替换或保存某个Element。
  • build() :子类有必要重写的笼统办法,回来一个新的Widget目标。一般只在三种情况下调用:Widget第一次刺进树中父级Widget更改配置依靠的InheritedWidget产生更改
  • createElement() :创立与此Widget相关的 StatelessElement 目标。

3.2. StatelessElement

class StatelessElement extends ComponentElement {
   StatelessElement(StatelessWidget super.widget);
  @override
  Widget build() => (widget as StatelessWidget).build(this);
  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    // 用断语的原因:StatelessWidget是不变的,理论上新旧Widget应该是持平的,假如不等,或许是一个逻辑错误。l
    assert(widget == newWidget);	
    rebuild(force: true);
  }
}

源码解读

  • 结构办法传入一个StatelessWidget实例传递给它的父类。
  • build() :调用Widget的build(),实践构建UI的地方。
  • update() :当结构需求更新与Element相关的Widget时调用。
  • rebuild(force) :强制从头构建Element,即使Widget没有改动。

所以 StatelessElement 的效果:

担任将StatelessWidget描绘的UI声明转换为实践的 界面,并 办理 这些界面的 生命周期和更新进程

3.3. BuildContext

对Widget树上某一个方位的引证,用于查找、拜访和操作该方位上的相关信息。每个Widget都有一个与之对应的 BuildContext,它指示了 该Widget在Widget树中的方位。然后源码里有这样一句注释:

/// [BuildContext] objects are actually [Element] objects. The [BuildContext] /// interface is used to discourage direct manipulation of [Element] objects.

简略翻译下:BuildContext 实践上便是 Element目标,界说此接口是为了阻挠对Element目标的直接调用。能够看到 Element 完成了这个 笼统类

abstract class Element extends DiagnosticableTree implements BuildContext { }

综上,BuildContext目标Element目标引证,供给了 构建Widget树时与Element进行交互的方式,如:查找Element的RenderObject、注册依靠联系、获取先人Widget等操作。一些常用的API:

  • Theme.of(context) : 获取最近的 Theme,用于获取当时的主题信息。
  • MediaQuery.of(context) : 获取最近的 MediaQuery,用于获取媒体查询信息,如设备尺度、设备像素比、屏幕亮度等。
  • Navigator.of(context) : 获取最近的 Navigator 状况,用于页面导航。
  • Scaffold.of(context) : 获取最近的 Scaffold 状况,用于显示 SnackBar、打开 Drawer 等。
  • Form.of(context) : 获取最近的 Form 状况,用于表单验证和保存。
  • BuildContext.dependOnInheritedWidgetOfExactType(): 获取最近的指定类型的 InheritedWidget。
  • Provider.of(context) :当运用 provider 包来进行状况办理时,用于获取最近的供给者 T。
  • BuildContext.findRenderObject() : 获取与当时 BuildContext 相相关的 RenderObject,这能够用来获取 widget 的尺度和方位。
  • BuildContext.owner: 获取当时 BuildContext 的 Element 所属的 BuildOwner,这一般用于结构内部。
  • BuildContext.widget: 获取与当时 BuildContext 相相关的 widget。

另外,需求留意,不同的API需求在Widget的不同生命周期调用,否则或许会抛出异常。如:不能在 build() 中直接调用 Scaffold.of(context) ,因为在构建进程中,Scaffold 或许还没有被创立。这种情况下,能够运用Builder 来获取子树的Context,或许运用 ScaffoldMessenger。示例如下:

@override
Widget build(BuildContext context) {
  // 这儿调用或许会抛出异常,因为Scaffold或许还没有被创立
  // Scaffold.of(context).openDrawer(); 
  // 运用Builder来正确获取context
  return Builder(
    builder: (BuildContext context) {
      return GestureDetector(
        onTap: () {
          Scaffold.of(context).openDrawer(); // 正确的调用
        },
        child: Text('Open Drawer'),
      );
    },
  );
}

4. StatefulWidget

需求保存状况,且或许呈现状况改动,与之相关的State目标状况产生改动,它都会从头构建自己。

4.1. 源码

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ super.key });
  @override
  StatefulElement createElement() => StatefulElement(this);
  @protected
  @factory
  State createState();
}

源码解读

  • createElement() :创立一个 StatefulElement 实例,用于办理 StatefulWidget的状况(State目标),并在适宜的时候与其通信。
  • createState() :子类有必要重写的笼统办法,当结构需求StatefulWidget的状况时会调用。结构或许会在StatefulWidget的生命周期内屡次调用此办法,比方:在树中的多个方位刺进此Widget,结构会为每个方位创立一个单独的State。

4.2. StatefulElement

它是 Widget和State间的桥梁,看下源码:

class StatefulElement extends ComponentElement {
    StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    state._element = this;
    state._widget = widget;
  }
  @override
  Widget build() => state.build(this);
  @override
  void reassemble() {
    state.reassemble();
    super.reassemble();
  }
  @override
  void _firstBuild() {
    state.didChangeDependencies();
    super._firstBuild();
  }
  @override
  void performRebuild() {
    if (_didChangeDependencies) {
      state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }
  @override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
    final StatefulWidget oldWidget = state._widget!;
    state._widget = widget as StatefulWidget;
    rebuild(force: true);
  }
  @override
  void activate() {
    super.activate();
    state.activate();
    markNeedsBuild();
  }
  @override
  void deactivate() {
    state.deactivate();
    super.deactivate();
  }
  @override
  void unmount() {
    super.unmount();
    state.dispose();
    state._element = null;
    _state = null;
  }
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _didChangeDependencies = true;
  }
}

源码解读

  • 结构办法: 传入一个StatefulWidget实例,并调用它的 createState() 创立一些State目标,并赋值给 _state。调用父类ComponentElement的结构函数,将State目标的 _element 设置为当时的StatefulElement实例,即完结 State目标与Element的相关。最终设置 _widget 为传入的StatefulWidget实例。
  • build() :调用State目标的build(),并传递当时Element,在这儿完结 实践Widget的构建
  • reassemble() :用于 热重载 的场景,先调用State的reassemble(),再调用父类的reassemble()。
  • _firstBuild()Element第一次被构建时调用,告诉State目标依靠联系现已改动,然后调父类_firstBuild()。
  • performRebuild()从头构建Widget时调用,假如 _didChangeDependencies 标志位设置为true,则会告诉State目标依靠联系现已改动,并将标志重置为false,然后调父类的 performRebuild()。
  • update() :将新的Widget与当时的Element相关,先调父类的update(),然后更新State目标中保存的widget引证,并强制从头构建。
  • activate() :Element从头刺进到树中时调用,它会调用State目标的activate(),并符号为需求从头构建。
  • deactivate() :当Element永久从树中移除时调用,它会调用State目标的 dispose() 来整理资源,然后断开State目标与Element的相关,并清空State目标的引证。
  • didChangeDependencies() :Element的依靠联系产生改动时调用,会将didChangeDependencies标志设置为true。

4.3. State

它是与 StatefulWidget相相关的状况目标的基类,看下源码:

// 此注解表示泛型类型参数T是可选的,混入Diagnosticable用于供给调试功用
@optionalTypeArgs
abstract class State<T extends StatefulWidget> with Diagnosticable {
  // 与State相关的StatefulWidget目标
  T get widget => _widget!;
  T? _widget;
  // 获取context的话,回来BuildContext的内部运用 → element
  BuildContext get context { return _element!  }
  StatefulElement? _element;
  // 判别当时State目标是否还挂载在Element树上
  bool get mounted => _element != null;
  // @mustCallSuper:子类重写此办法时有必要调用父类的办法
  @protected
  @mustCallSuper
  void initState() {
    //...
  }
  @mustCallSuper
  @protected
  void didUpdateWidget(covariant T oldWidget) { }
  @protected
  @mustCallSuper
  void reassemble() { }
  @protected
  void setState(VoidCallback fn) {
    // 将当时与State目标相关的Element符号为"dirty"(需求重建)
    _element!.markNeedsBuild();
  }
  @protected
  @mustCallSuper
  void deactivate() { }
  @protected
  @mustCallSuper
  void activate() { }
  @protected
  @mustCallSuper
  void dispose() {
    //...
  }
  @protected
  Widget build(BuildContext context);
  @protected
  @mustCallSuper
  void didChangeDependencies() { }
}
// 附:Element#markNeedsBuild() 的代码:
void markNeedsBuild() {
  if (_lifecycleState < _ElementLifecycle.inactive) {
    // 假如Element还没有被激活(也便是还没有参加到树中),则不做任何事情。
    return;
  }
  if (!owner!._debugBuilding) {
    // 假如当时不在构建阶段,则将Element符号为dirty,并且参加到owner的dirty elements队列中。
    _dirty = true;
    // _scheduleBuildFor 会确保鄙人一个动画帧Element会被重建。
    // 这个进程是异步的,意味着setState()后,UI不会当即更新,而是鄙人一个才重建并显示最新的状况
    owner!._scheduleBuildFor(this);
  } else {
    // 假如当时现已在构建阶段,阐明你正试图在build办法中调用setState,这是不答应的。
    assert(() {
      throw FlutterError(
        'Cannot markNeedsBuild() during the build phase.n'
        'markNeedsBuild() was called during the build phase for the widget: $this.'
      );
    }());
  }
}

源码解读

  • initState() :当State目标被创立并刺进到树中时调用,用于初始化设置,如:监听器、初始化数据等。
  • didUpdateWidget() :当State目标相关的Widget在树中方位不变,但配置产生改动时调用,用于响应与Widget相关的配置改动。父Widget重建并创立新的Widget传递给相同的State时调用,在build()后履行。
  • setState() :主动告诉结构State目标的内部状况现已改动,结构需求重建这个Widget。
  • deactivate() :当State目标从树中暂时移除时调用,一旦调用此办法,目标或许被销毁,也或许被从头刺进到树中。
  • activate() :当State目标从头刺进到树中调用。
  • dispose() :当State目标被永久移除时调用,一般在此进行资源释放作业,如:取消监听器、定时器等。
  • build() :子类有必要完成的笼统办法,依据当时的状况构建UI,每次调setState()后都会调用。
  • didChangeDependencies() :当State目标的依靠的InheritedWidget产生改动时调用。

4.4. 调用流程/生命周期

相关办法都了解得差不多了,接着简略画个图了解调用进程/生命周期:

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

结合源码和流程图,相信你应该能回答这个问题 → 为什么Widget重建后,State没有丢失?

答:State目标是StatefulElement持有的,当StatefulWidget重建时,新Widget实例会与原来的StatefulElement进行匹配,Flutter结构会调用 Element#update() 来相关新的Widget实例。整个进程,StatefulElement 会坚持原有的State目标不变,因而,一切状况信息都被保存下来啦~

5. InheritedWidget

ProxyWidget 是一个笼统类,效果如其名 → 作为其它Widget的署理,答应开发者在子Widget构建中刺进一些额外逻辑。源码如下:

abstract class ProxyWidget extends Widget {
  const ProxyWidget({ super.key, required this.child });
  final Widget child;
}

就一个child特点,也忒简练了吧,一般不会直接运用它,而是运用它的两个子类 → InheritedWidgetParentDataWidget,本节首要了解前者~

5.1. 简略运用示例

能够高效地将 同享的数据传递给子Widget树,不需求 手动将数据经过结构函数一层层往下传递。这个组件之前我们没用过,写个简略代码示例~

import 'package:flutter/material.dart';
// 界说一个简略的Theme类来持有色彩数据
class Theme {
  final Color primaryColor;
  Theme({required this.primaryColor});
}
// 创立一个InheritedWidget来传递Theme
class ThemeInheritedWidget extends InheritedWidget {
  // 需求同享的数据
  final Theme theme;
  const ThemeInheritedWidget({super.key, required this.theme, required super.child});
  // 界说一个快捷办法,便利子widget获取最近的ThemeInheritedWidget实例
  static ThemeInheritedWidget? of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<ThemeInheritedWidget>();
  // 当InheritedWidget更新时,决议是否告诉依靠它的子widget,这儿判别theme目标是否改动
  @override
  bool updateShouldNotify(ThemeInheritedWidget oldWidget) => theme != oldWidget.theme;
}
// 运用主题色的StatefulWidget
class ThemedContainer extends StatefulWidget {
  const ThemedContainer({super.key});
  @override
  State<StatefulWidget> createState() => ThemedContainerState();
}
class ThemedContainerState extends State<ThemedContainer> {
  @override
  Widget build(BuildContext context) {
    // 获取最近的ThemeInheritedWidget
    final theme = ThemeInheritedWidget.of(context)?.theme;
    return Container(
      height: 100,
      width: 100,
      color: theme?.primaryColor ?? Colors.white, // 运用InheritedWidget供给的色彩
    );
  }
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies() is Called");
  }
}
// 运用主题色的StatelessWidget
class ThemedText extends StatelessWidget {
  const ThemedText({super.key});
  @override
  Widget build(BuildContext context) {
    // 获取最近的ThemeInheritedWidget
    final theme = ThemeInheritedWidget.of(context)?.theme;
    return Text(
      'This text is themed',
      style: TextStyle(color: theme?.primaryColor ?? Colors.black),
    );
  }
}
// 调用处
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 运用ThemeInheritedWidget包裹MaterialApp,供给主题数据
    return ThemeInheritedWidget(
      theme: Theme(primaryColor: Colors.blue),
      child: MaterialApp(
        title: 'InheritedWidget Example',
        home: Scaffold(
          appBar: AppBar(
            title: Text('InheritedWidget Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // 运用承继的主题色彩
                ThemedContainer(),
                // 另一个运用承继的主题色彩的widget
                ThemedText(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
void main() {
  runApp(MyApp());
}

运转输出结果

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

一起,操控台输出了 didChangeDependencies() is Called,阐明这个回调办法的确在 “依靠” 改动时被Flutter结构调用了。这儿的”依靠”指的是:子Widget是否运用了父Widget中的数据

  • 假如删掉 ThemeInheritedWidget.of(context)?.theme 这段代码,即不依靠父Widget中的数据,这个办法将不会被调用。
  • 一般很少重写这个办法,除非是你需求在依靠改动后履行一些”贵重”操作,如:网络恳求,能够在此办法中履行,这样能够防止每次build()都履行这些操作。

‍♂️ 另外,假如只想引证 ThemeInheritedWidget 里供给的同享数据,但不希望它产生改动时调用ThemedContainerStatedidChangeDependencies() ,能够改为调另一个函数:

dependOnInheritedWidgetOfExactType()
// 改为:
getElementForInheritedWidgetOfExactType<ThemeInheritedWidget>()!.widget as ThemeInheritedWidget 

详细原理,等下看源码你就知道了~

5.2. 源码

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ super.key, required super.child });
  @override
  InheritedElement createElement() => InheritedElement(this);
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

源码解读

  • createElement() :回来了一个 InheritedElement 实例。
  • updateShouldNotify() :子类按需重写,回来bool值,表示 InheritedWidget 更新时 是否告诉依靠它的子Widget从头构建。

5.3. InheritedElement

class InheritedElement extends ProxyElement {
  InheritedElement(InheritedWidget super.widget);
  final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
  }
  @override
  void updated(InheritedWidget oldWidget) {
    if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
      super.updated(oldWidget);
    }
  }
  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
}
abstract class ProxyElement extends ComponentElement {
  ProxyElement(ProxyWidget super.widget);
  @override
  Widget build() => (widget as ProxyWidget).child;
  @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget as ProxyWidget;
    super.update(newWidget);
    updated(oldWidget);
    rebuild(force: true);
  }
  @protected
  void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
  }
  @protected
  void notifyClients(covariant ProxyWidget oldWidget);
}

源码解读

  • InheritedWidget 需求更新时,父类 ProxyElement#update() 会被调用,其中调用了 updated()。
  • InheritedWidget#updated() 判别 updateShouldNotify() 是否回来true,是调用父类ProxyElement#updated(), 其中调用了 notifyClients() 遍历依靠InheritedWidget的一切Element,调用 notifyDependent() 进行告诉,详细调用的 Element#didChangeDependencies() 触发重建

顺带看看 Element类dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType() 的区别:

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
  if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget as InheritedWidget;
}
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T];
  return ancestor;
}

能够看到前者多调了一个 dependOnInheritedElement() ,在其中注册了依靠联系,这便是为啥运用后者就不会回调 didChangeDependencies() 的原因。

5.4. 调用流程

源码了解得差不多了,接着捋下调用流程~

跟杰哥一同学Flutter (十三、从Widget源码 ✈ InheritedWidget)

6. 小结:Widget重建流程

经过跟踪源码,能够发现:无论是 主动调用setState() 传入匿名函数更新状况,仍是 InheritedWidget 在同享数据产生改动时遍历一切依靠的子Widget的 didChangeDependencies() ,终究都是调用的 markNeedsBuild() ,该办法首要做了两件事:

  • _dirty = true → 符号此Element为”dirty”,即状况改动需求重建,需求鄙人一帧中更新UI。
  • owner!._scheduleBuildFor(this); → 组织重建,将此Element参加结构的 重建队列。这儿的owner指的是Element的 BuildOwner,它担任办理一个Widget的构建进程。

在每个动画帧的开端,Flutter结构会调用 BuildOwner#buildScope() 创立一个 上下文(Scope) 来办理当时帧中一切需求重建的 Element:

  • 遍历待处理队列中一切符号为”dirty”的Element。
  • 按照一定的次序 (一般是从顶部到底部) 履行它们的 rebuild() 办法进行重建。
  • 办法内部会调用 Widget#build() ,这样,Widget 就会依据最新的状况被重建。