天天看點

Widget原理分析

目錄介紹

  • 01.Widget基礎概念
    • 1.1 Widget概念
    • 1.2 Widget骨架
    • 1.3 Widget源碼
    • 1.4 Widget不可變
  • 02.StatelessWidget源碼
  • 03.StatefulWidget源碼
  • 04.InheritedWidget源碼
  • 05.Context是什麼作用

  • 在Flutter中幾乎所有的對象都是一個Widget。
    • 與原生開發中“控件”不同的是,Flutter中的Widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的元件如:用于手勢檢測的

      GestureDetector

      widget、用于APP主題資料傳遞的

      Theme

      等等,而原生開發中的控件通常隻是指UI元素。
    • 在描述UI元素時可能會用到“控件”、“元件”這樣的概念,讀者心裡需要知道他們就是widget,隻是在不同場景的不同表述而已。
    • 由于Flutter主要就是用于建構使用者界面的,是以,在大多數時候,可以認為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 對象實作狀态的儲存。

  • 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

      類繼承自

      DiagnosticableTree

      DiagnosticableTree

      即“診斷樹”,主要作用是提供調試資訊。
    • 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

      對象的配置;通過其源碼我們可以看到,隻要

      newWidget

      oldWidget

      runtimeType

      key

      同時相等時就會用

      newWidget

      去更新

      Element

      對象的配置,否則就會建立新的

      Element

  • 核心方法createElement()
    • Widget

      類本身是一個抽象類,其中最核心的就是定義了

      createElement()

      接口。
    • 在Flutter開發中,我們一般都不用直接繼承

      Widget

      類來實作一個新元件,相反,我們通常會通過繼承

      StatelessWidget

      StatefulWidget

      來間接繼承

      Widget

      類來實作。
    • StatelessWidget

      StatefulWidget

      都是直接繼承自

      Widget

      類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型。
  • 核心方法canUpdate()
    • 實際上就是比較兩個 widget 的 runtimeType 和 key 是否相同。
      • runtimeType 也就是類型,如果新老 widget 的類型都變了,顯然需要重新建立 Element。
      • key Flutter 中另一個核心的概念,key 的存在影響了 widget 的更新、複用流程,這裡先不展開。
    • 預設情況下 widget 建立時不需傳入 key,是以更多情況下隻需要比較二者的類型,如果類型一樣,那麼目前節點的 Element 不需要重建,接下來繼續調用 child.update 更新子樹。

  • 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。

  • 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。

  • 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對象到樹中”這樣的描述。
    • 其實,無論哪種描述,其意思都是在描述“一棵構成使用者界面的節點元素的樹”,如果沒有特别說明,都可抽象的認為它是“一棵構成使用者界面的節點元素的樹”。

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 整體的注冊、通知流程結束。

  • 什麼是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

繼續閱讀