Widget原理分析

简介: Widget原理分析

目录介绍

  • 01.Widget基础概念

    • 1.1 Widget概念
    • 1.2 Widget骨架
    • 1.3 Widget源码
    • 1.4 Widget不可变
  • 02.StatelessWidget源码
  • 03.StatefulWidget源码
  • 04.InheritedWidget源码
  • 05.Context是什么作用

01.Widget基础概念

1.1 Widget概念

  • 在Flutter中几乎所有的对象都是一个Widget。

    • 与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等,而原生开发中的控件通常只是指UI元素。
    • 在描述UI元素时可能会用到“控件”、“组件”这样的概念,读者心里需要知道他们就是widget,只是在不同场景的不同表述而已。
    • 由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,可以认为widget就是一个控件,不必纠结于概念。

1.2 Widget骨架

  • Widget 的骨架

    • 常用的 StatefulWidget、StatelessWidget,再加上 (InheritedWidget) 或 ProxyWidget 和 RenderObjectWidget 都继承于 Widget 基类,他们整体构成了 Widget 的骨架。
  • 有状态 和 无状态

    • StatelessWidget 无状态的 Widget,常见的子类如 Text、Container。
    • StatefulWidget 有状态的 Widget,常用的子类有 Image、Navigator。
    • ProxyWidget 为代理 Widget,可以快速追溯父节点,通常用来做数据共享,常见的子类 InheritedWidget,各种状态管理框架,如 provider 等正是基于它实现。
    • 什么叫做“状态”?Widget 在 Flutter 架构下设计为不可变的,通常情况下每一帧都会重新构建一个新的 Widget 对象,而无法知道之前的状态。StatefulWidget 通过关联一个 State 对象实现状态的保存。

1.3 Widget源码

  • Widget源码如下所示

    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';
      }
    
      @override
      void debugFillProperties(DiagnosticPropertiesBuilder properties) {
        super.debugFillProperties(properties);
        properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
      }
    
      @override
      @nonVirtual
      bool operator ==(Object other) => super == other;
    
      @override
      @nonVirtual
      int get hashCode => super.hashCode;
    
      static bool canUpdate(Widget oldWidget, Widget newWidget) {
        return oldWidget.runtimeType == newWidget.runtimeType
            && oldWidget.key == newWidget.key;
      }
    
      static int _debugConcreteSubtype(Widget widget) {
        return widget is StatefulWidget ? 1 :
               widget is StatelessWidget ? 2 :
               0;
        }
    }
  • 主要方法和属性介绍

    • Widget类继承自DiagnosticableTreeDiagnosticableTree即“诊断树”,主要作用是提供调试信息。
    • Key: 这个key属性类似于React/Vue中的key,主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在canUpdate()方法中。
    • createElement():正如所述“一个Widget可以对应一个Element”;Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
    • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性。
    • canUpdate(...)是一个静态方法,它主要用于在Widget树重新build时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element
  • 核心方法createElement()

    • Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口。
    • 在Flutter开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidgetStatefulWidget来间接继承Widget类来实现。
    • StatelessWidgetStatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型。
  • 核心方法canUpdate()

    • 实际上就是比较两个 widget 的 runtimeType 和 key 是否相同。

      • runtimeType 也就是类型,如果新老 widget 的类型都变了,显然需要重新创建 Element。
      • key Flutter 中另一个核心的概念,key 的存在影响了 widget 的更新、复用流程,这里先不展开。
    • 默认情况下 widget 创建时不需传入 key,因此更多情况下只需要比较二者的类型,如果类型一样,那么当前节点的 Element 不需要重建,接下来继续调用 child.update 更新子树。

1.4 Widget不可变

  • Widget 是一个很重要的概念,但是Widget有一个更重重要的特性,就是Widget是immutable(不可变的),这是什么意思?

    • 拿 Opacity 为例给讲解,讲解之前先看一下Opacity的继承关系。(在讲源码之前我们先看一下Opacity的职责是什么,Opacity是一个能让他的孩子透明的组件,很简单也很容易理解。)
    • Opacity继承自SingleChildRenderObjectWidget,这类只包含了一个child的Widget,它继承自RenderObjectWidget,RenderObjectWidget继承自Widget。
    class Opacity extends SingleChildRenderObjectWidget {
    }
    
    abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
    }
    
    abstract class RenderObjectWidget extends Widget {
    }
  • 然后看一下 Opacity 简单源码

    class Opacity extends SingleChildRenderObjectWidget {
      
      const Opacity({
        Key key,
        @required this.opacity,
        Widget child,
      }) : super(key: key, child: child);
      
      final double opacity;//注释1
      
      @override
      RenderOpacity createRenderObject(BuildContext context) {//注释2
        return RenderOpacity(
          opacity: opacity
        );
      }
    
      @override
      void updateRenderObject(BuildContext context, RenderOpacity renderObject) {
        renderObject
          ..opacity = opacity
      }
    }
    • 在注释1处声明了一个属性,这属性是final,也就除了构造函数能给这个属性赋值之外,没有其他的办法让这个值进行改变。那我们想改变这个值怎么办,唯一的办法就是创建一个新的Opacity。

02.StatelessWidget源码

  • StatelessWidget源码如下所示

    abstract class StatelessWidget extends Widget {
      const StatelessWidget({ Key key }) : super(key: key);
    
      @override
      StatelessElement createElement() => StatelessElement(this);
    
      @protected
      Widget build(BuildContext context);
    }
  • StatelessWidget相对比较简单,它继承自Widget类,重写了createElement() 方法。

    • StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。
  • StatelessWidget用于不需要维护状态的场景。

    • 它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

03.StatefulWidget源码

  • StatefulWidget源码如下所示

    abstract class StatefulWidget extends Widget {
      const StatefulWidget({ Key key }) : super(key: key);
    
      @override
      StatefulElement createElement() => StatefulElement(this);
    
      @protected
      @factory
      State createState();
    }
  • StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。

    • StatefulElement 中可能会多次调用createState()来创建状态(State)对象。
  • createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。

    • 例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。
  • 理解树的概念

    • 在不同的场景可能指不同的意思,在说“widget树”时它可以指widget结构树,但由于widget与Element有对应关系(一可能对多)。
    • 在有些场景(Flutter的SDK文档中)也代指“UI树”的意思。
    • 而在stateful widget中,State对象也和StatefulElement具有对应关系(一对一),所以在Flutter的SDK文档中,可以经常看到“从树中移除State对象”或“插入State对象到树中”这样的描述。
    • 其实,无论哪种描述,其意思都是在描述“一棵构成用户界面的节点元素的树”,如果没有特别说明,都可抽象的认为它是“一棵构成用户界面的节点元素的树”。

04.InheritedWidget源码

4.1 InheritedWidget源码分析

  • InheritedWidget源码如下所示

    abstract class InheritedWidget extends ProxyWidget {
    
      const InheritedWidget({ Key key, Widget child })
        : super(key: key, child: child);
    
      @override
      InheritedElement createElement() => InheritedElement(this);
    
      @protected
      bool updateShouldNotify(covariant InheritedWidget oldWidget);
    }
  • 如果想自己实现一个类似主题变更后,更新相应 UI 的功能应该怎么做?

    • 大致思路应该就是一个观察者模式,凡是使用的主题数据的地方,需要向 主题中心 注册一个观察者,当主题数据发生 改变 时,主题中心依次通知各个观察者进行 UI 更新。
    • 这里有个问题需要解决,如何定义 数据改变 ?事实上,数据是否改变是由业务方决定的,因此这里需要抽象出相应接口,来看 InheritedWidget 结构。
  • 核心就是 updateShouldNotify 方法

    • 入参为原始的 widget,返回值为布尔值,业务方需要实现此方法,判断是否需要将变化通知到各个观察者。

4.2 注册和通知流程

  • 举一个常见例子

    • 比如,app设置了theme主题,当修改了theme主题颜色,是怎么修改全局所有页面的theme状态的呢?这个就用到了注册和通知的功能,接着往下看:
  • 注册流程

    • 假设 StudyWidget 是我们业务侧的 Widget,其内部使用了 Theme.of(context) 方法获取任意主题信息后,会经一系列调用,最终将这个 context——StudyWidget 对应的 Element 对象,注册到 InheritedElement的成员变量 Map<Element, Object> _dependents 中。
    • 另外需要注意,之所以在第二步中,可以找到父 InheritedElement,是因为在 Element 的 mount 过程中,会将父 Widget 中保存的 Map<Type, InheritedElement> _inheritedWidgets 集合,依次传递给子 Widget。如果自身也是 InheritedElement 也会添加到这个集合中。
    • 当我们使用 MaterialApp 或 CupertinoApp 作为根节点时,其内部已经帮我们封装了一个 Theme Widget,因此不需要我们额外的套一层作为注册了。
  • 通知流程

    • 当父 InheritedWidget 发生状态改变时,最终会调用到 InheritedElement 的 update 方法,我们以此作为通知的起点。
    • 可以看到,流程最终会将依赖的 Element 标脏,在下一帧重绘时将会更新对应 Widget 的状态。至此,InheritedWidget 整体的注册、通知流程结束。

05.Context是什么作用

  • 什么是Context

    • build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。
    • 实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。
  • 下面是在子树中获取父级widget的一个示例:

    class ContextRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Context测试"),
          ),
          body: Container(
            child: Builder(builder: (context) {
              // 在Widget树中向上查找最近的父级`Scaffold` widget
              Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
              // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
              return (scaffold.appBar as AppBar).title;
            }),
          ),
        );
      }
    }

推荐:https://github.com/yangchong211/YCFlutterUtils

目录
相关文章
|
4月前
|
Android开发
Android基础知识:什么是Fragment?与Activity的区别是什么?
Android基础知识:什么是Fragment?与Activity的区别是什么?
318 54
|
5月前
|
Android开发 容器
[Android]View的事件分发机制(源码解析)
[Android]View的事件分发机制(源码解析)
37 0
|
9月前
Flutter源码分析笔记:Widget类源码分析
本文记录阅读与分析Flutter源码 - Widget类源码分析。
62 0
Flutter源码分析笔记:Widget类源码分析
|
12月前
|
XML Java Android开发
Android——RecyclerView简单实现及Viewbinding优化
本文是博主对Adapter(适配器)的一些理解,为了加深对Adapter的理解以及记录自己的阶段学习而写,同时也适合初学者阅读,参考本条博客的逻辑进行学习。
306 0
|
存储 前端开发 Android开发
Android | LiveData 源码分析
Android | LiveData 源码分析
|
Android开发 容器
View工作原理分析1 - 初识ViewRoot和 DecorView
以下相关资料均来自 Android艺术探索,部分内容加入了一些我个人的理解。
107 0
|
Android开发 容器
Android VSYNC (Choreographer)与UI刷新原理分析
Android VSYNC (Choreographer)与UI刷新原理分析
392 0
Android VSYNC (Choreographer)与UI刷新原理分析
DHL
|
设计模式 算法 安全
0xA05 Android 10 源码分析:Dialog加载绘制流程以及在Kotlin、DataBinding中的使用
0xA05 Android 10 源码分析:Dialog加载绘制流程以及在Kotlin、DataBinding中的使用
DHL
353 0
0xA05 Android 10 源码分析:Dialog加载绘制流程以及在Kotlin、DataBinding中的使用
|
Java Android开发 Python
View事件机制源码分析
目录介绍 01.Android中事件分发顺序 02.Activity的事件分发机制 2.1 源码分析 2.2 点击事件调用顺序 2.3 得出结论 03.ViewGroup事件的分发机制 3.
831 0