Flutter之萬物皆Widget(一種你沒見過的方式來深入Widget)

背景

為什么說Flutter萬物皆Widget?首先你要知道,F(xiàn)lutter是什么,它是一個現(xiàn)代的響應(yīng)式框架、一個2D渲染引擎、現(xiàn)成的widget和開發(fā)工具,基于Skia,一個性能彪悍的2D圖像繪制引擎,2005年被Google收購,被廣泛應(yīng)用于Chrome和Android之上,等等吧,說白一點,F(xiàn)lutter就是一個UI框架,所以說,萬物皆Widget,而Widget的中文意思是小部件,它為什么不能像Android或者Ios一樣叫做View呢?因為widget既可以是一個結(jié)構(gòu)元素(如按鈕或菜單)、也可以是一個文本樣式元素(如字體或顏色方案)、布局的一個方面(如填充)等等,我們可以統(tǒng)籌它們?yōu)閣iget,而不是view,根據(jù)基本的命名規(guī)范,這就是一種合理的命名抽象。那么接下來我們學(xué)什么?

  • Widget是什么
  • Widget類結(jié)構(gòu)
  • 跟著我實現(xiàn)一個widget(直接繼承widget抽象類)
  • Element類結(jié)構(gòu)
  • 深入理解Element

Widget是什么

其實上面說了,一切皆Widget,那我們可不可以認(rèn)為,在flutter的框架中,用到的東西都是Widget呢,當(dāng)然不是哈,由于它是基于Dart,所以有很多Dart的庫,還是可以使用的,比如AES,RSA加密解密,Json序列化等等,但你可以這么說,一切構(gòu)建圖形相關(guān)的東西都是Widget,這就是Widget

Widget類結(jié)構(gòu)

為什么說下類結(jié)構(gòu)呢?類結(jié)構(gòu)可以很清晰幫助我們梳理邏輯,從全局的角度看待整個結(jié)構(gòu)

image
  • RenderObjectWidget 看名字我們判斷,它是持有RenderObject對象的Widget,而通過其他通道了解到,RenderObject實際上是完成界面的布局、測量與繪制,像Padding,Table,Align都是它的子類
  • StatefulWidget 多了一個State狀態(tài)的Widget,子類都是可以動態(tài)改變的如CheckBox,Switch
  • StatelessWidget 就是一個普通的Widget,不可變?nèi)鏘con,Text。
  • ProxyWidget InheritedWidget就是它的子類,我們暫且認(rèn)為它是子類能從父類拿數(shù)據(jù)的關(guān)鍵,以后再研究,大多數(shù)的主題都是繼承自ProxyWidget

跟我一起實現(xiàn)一個Widget

我不想和別人的教程思路一樣,既然萬物皆Widget,那我們就從實現(xiàn)一個Widget開始,然后一步步深入,看到什么就去了解什么?來上代碼

class TestWidget extends Widget{
  @override
  Element createElement() {
    // TODO: implement createElement
    throw UnimplementedError();
  }
}

創(chuàng)建一個TestWidget然后繼承Widget,然后會讓你重寫函數(shù)createElement,返回一個Element,通過這個我們看的出,其實我們創(chuàng)建的Widget,最終肯定是創(chuàng)建了一個Element,那Element到底是什么呢?同樣的思路,我們繼承Element看一下

class TestElement extends Element{

  TestElement(Widget widget) : super(widget);

  @override
  bool get debugDoingBuild => throw UnimplementedError();

  @override
  void performRebuild() {
  }

}

多了一個構(gòu)造函數(shù),傳遞Widget對象,get函數(shù)debugDoingBuild,還有performRebuild函數(shù),都是干嘛的呢?

abstract class Element extends DiagnosticableTree implements BuildContext 

abstract class BuildContext {

  /// Whether the [widget] is currently updating the widget or render tree.
  ///
  /// For [StatefulWidget]s and [StatelessWidget]s this flag is true while
  /// their respective build methods are executing.
  /// [RenderObjectWidget]s set this to true while creating or configuring their
  /// associated [RenderObject]s.
  /// Other [Widget] types may set this to true for conceptually similar phases
  /// of their lifecycle.
  ///
  /// When this is true, it is safe for [widget] to establish a dependency to an
  /// [InheritedWidget] by calling [dependOnInheritedElement] or
  /// [dependOnInheritedWidgetOfExactType].
  ///
  /// Accessing this flag in release mode is not valid.
  bool get debugDoingBuild;
   

經(jīng)過代碼的跟蹤我們發(fā)現(xiàn)一些注解:

  • Element繼承自DiagnosticableTree,并實現(xiàn)BuildContext
  • DiagnosticableTree是個“診斷樹”,主要作用是提供調(diào)試信息。
  • BuildContext類似原生系統(tǒng)的上下文,它定義了debugDoingBuild,通過注解我們知道,它應(yīng)該就是一個debug用的一個標(biāo)志位。
  • performRebuild 經(jīng)過源碼查看后發(fā)現(xiàn),由rebuild()調(diào)用如下
  void rebuild() {
     if (!_active || !_dirty)
      return;
    performRebuild();
  }
  
    @override
  void update(ProxyWidget newWidget) {
    rebuild();
  }
  

首先說明下,這個并不是Element的源碼,我摘自StatelessElement,是Element的子類,這說明在update函數(shù)后,Element就會直接執(zhí)行performRebuild函數(shù),那我們完善下自定義的Element邏輯

class TestElement extends Element {

  TestElement(Widget widget) : super(widget);

  @override
  bool get debugDoingBuild => throw UnimplementedError();

  @override
  void performRebuild() {
  }

  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    print("TestWidget update");
    performRebuild();
  }

  @override
  TestWidget get widget => super.widget as TestWidget;

  Widget build() => widget.build(this);
}

在update的時候執(zhí)行performRebuild(),但是performRebuild執(zhí)行什么呢?我們結(jié)合一下StatelessElement的實現(xiàn),發(fā)現(xiàn),它調(diào)用了傳遞進(jìn)來的Widget參數(shù)build函數(shù),那么我們就在TestWidget中添加函數(shù),并完善下邏輯后是這樣的

class TestWidget extends Widget {

  @override
  Element createElement() {
    /// 將自己傳遞進(jìn)去,讓Element調(diào)用下面的build函數(shù)
    return TestElement(this);
  }
   /// 這個context其實就是Element
  Widget build(BuildContext context) {
    print("TestWidget build");
    return Text("TestWidget");
  }
}

class TestElement extends Element {

  Element _child;

  TestElement(Widget widget) : super(widget);

  @override
  bool get debugDoingBuild => throw UnimplementedError();

  @override
  void performRebuild() {
    ///調(diào)用build函數(shù)
    var _build = build();
    ///更新子視圖
   _child =  updateChild(_child, _build, slot);
  }

  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    print("TestWidget update");
    ///更新
    performRebuild();
  }

  ///將widget強轉(zhuǎn)成TestWidget
  @override
  TestWidget get widget => super.widget as TestWidget;
  /// 調(diào)用TestWidget的build函數(shù)
  Widget build() => widget.build(this);
}

然后將其放入main.dart中如圖

image

最終效果展示,如圖

[圖片上傳失敗...(image-72eee6-1600853501724)]

展示出來了,我們簡單總結(jié)一下,到目前你學(xué)到了什么?

  • Widget會創(chuàng)建Element對象(調(diào)用createElement并不是Widget,而是Framework)
  • Widget并沒有實際的操控UI
  • Element是在update的時候重新調(diào)用Widget的build函數(shù)來構(gòu)建子Widget
  • updateChild會根據(jù)傳入的Widget生成新的Element
  • Widget的函數(shù)build,傳入的context其實就是它創(chuàng)建的Element對象,那么為什么這么設(shè)計呢?一方面它可以隔離掉一些Element的細(xì)節(jié),避免Widget頻繁調(diào)用或者誤操作帶來的不確定問題,一方面context上下文可以存儲樹的結(jié)構(gòu),來從樹種查找元素。

其實可以很簡單的理解為,Widget就是Element的配置信息,在Dart虛擬機中會頻繁的創(chuàng)建和銷毀,由于量比較大,所以抽象一層Element來讀取配置信息,做一層過濾,最終再真實的繪制出來,這樣做的好處就是避免不必要的刷新。接下來我們深入了解下Element

Element類結(jié)構(gòu)

在深入了解Element之前我們也從全局看下它的結(jié)構(gòu)

image

可以看到,Element最主要的兩個抽象:

  • ComponentElement
  • RenderObjectElement

都是干嘛的呢?經(jīng)過看源碼,發(fā)現(xiàn)ComponentElement,其實做了一件事情就是在mount函數(shù)中,判斷Element是第一次創(chuàng)建,然后調(diào)用_firstBuild,最終通過rebuild調(diào)用performRebuild,通過上面我們也知道performRebuild最終調(diào)用updateChild來繪制UI
而RenderObjectElement就比較復(fù)雜一點,它創(chuàng)建了RenderObject,通過RenderObjectWidget的createRenderObject方法,通過以前的學(xué)習(xí),我們也知道RenderObject其實是真正繪制UI的對象,所以我們暫且認(rèn)為RenderObjectElement其實就是可以直接操控RenderObject,一種更直接的方式來控制UI。

深入理解Element

為什么要深入理解Element呢,由于大多數(shù)情況下,我們開發(fā)者并不會直接操作Element,但對于想要全局了解FlutterUI框架至關(guān)重要,特別實在一些狀態(tài)管理的框架中,如Provider,他們都定制了自己的Element實現(xiàn),那么這么重要,我們需要從哪方面了解呢?一個很重要的知識點就是生命周期,只有了解了正確的生命周期,你才能在合適的時間做合適的操作

image

為了驗證該圖,我們加入日志打印下,代碼如下:

/// 創(chuàng)建LifecycleElement 實現(xiàn)生命周期函數(shù)
class LifecycleElement extends TestElement{
  
  LifecycleElement(Widget widget) : super(widget);

  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
  }

  @override
  void unmount() {
    print("LifecycleElement unmount");
    super.unmount();
  }

  @override
  void activate() {
    print("LifecycleElement activate");
    super.activate();
  }

  @override
  void rebuild() {
    print("LifecycleElement rebuild");
    super.rebuild();
  }

  @override
  void deactivate() {
    print("LifecycleElement deactivate");
    super.deactivate();
  }

  @override
  void didChangeDependencies() {
    print("LifecycleElement didChangeDependencies");
    super.didChangeDependencies();
  }

  @override
  void update(Widget newWidget) {
    print("LifecycleElement update");
    super.update(newWidget);
  }

  @override
  Element updateChild(Element child, Widget newWidget, newSlot) {
    print("LifecycleElement updateChild");
    return super.updateChild(child, newWidget, newSlot);
  }

  @override
  void deactivateChild(Element child) {
    print("LifecycleElement deactivateChild");
    super.deactivateChild(child);
  }

}

class TestWidget extends Widget {

  @override
  Element createElement() {
    /// 將自己傳遞進(jìn)去,讓Element調(diào)用下面的build函數(shù)
    /// 更新TestElement為LifecycleElement
    return LifecycleElement(this);
  }
  /// 這個context其實就是Element
  Widget build(BuildContext context) {
    return Text("TestWidget");
  }
}

然后改造下main.dart, 如下

///添加變量
  bool isShow = true;
/// 加入變量控制
  isShow ? TestWidget() : Container(),
/// 將floatingActionButton改為這樣的實現(xiàn)
 onPressed: () {
          setState(() {
            isShow = !isShow;
          });
        },

運行一下項目查看日志

image
  • 調(diào)用 element.mount(parentElement,newSlot)
  • 調(diào)用 update(Widget newWidget)
  • 調(diào)用 updateChild(Element child, Widget newWidget, newSlot)

然后我們點擊下按鈕

image
  • 調(diào)用 deactivate()
  • 調(diào)用 unmount()

我們再點擊下按鈕

image

這次只有mount,為什么?由于Widget本身不可變,我判斷是因為這個導(dǎo)致的,那如何判斷呢?下面介紹一個小技巧,其實flutter的framework層是可以加入調(diào)試代碼的,我們加入日志看下,如下:

/// widget 基類其實有一個canUpdate函數(shù),我們猜測肯定是這里導(dǎo)致的,加入日志如下
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    
    if(oldWidget.toString()=="TestWidget") {
      print("canUpdate${oldWidget.runtimeType == newWidget.runtimeType
          && oldWidget.key == newWidget.key}");
    }

    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

是個靜態(tài)函數(shù),肯定是在Element中被調(diào)用的,我們找下

@mustCallSuper
  void update(covariant Widget newWidget) {
  
     if (newWidget.toString() == "TestWidget") {
      print("TestWidget update start");
    }
  
    assert(_debugLifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && _active
        && Widget.canUpdate(widget, newWidget));

    assert(() {
      _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
      _debugForgottenChildrenWithGlobalKey.clear();
      return true;
    }());
      if (newWidget.toString() == "TestWidget") {
      print("TestWidget:${newWidget.hashCode}");
    }
    _widget = newWidget;
  }

如上代碼是Element的源碼,這里調(diào)用了canUpdate函數(shù),如果不需要更新的話,就直接中斷了執(zhí)行,我們重新運行下demo,并在加一個print來驗證一下newWidget是什么樣子的,這里加入newWidget.toString() == "TestWidget",主要是為了過濾垃圾日志,重新運行項目。如圖

image

點擊后按鈕

image

再點擊

image

發(fā)現(xiàn)并沒有調(diào)用canUpdate,那我們?nèi)绾巫屗匦录虞d回來呢?我們查查資料,改造下例子

  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
    assert(_child == null);
    print("LifecycleElement firstBuild");
    performRebuild();
  }

mount函數(shù)加入performRebuild()函數(shù),最終會觸發(fā)updateChild,加assert斷言是防止后面再加載進(jìn)來的時候多次觸發(fā)updateChild,然后改造下main.dart

@override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: isShow ? TestWidget() : Container(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            isShow = !isShow;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

去掉Column,這里是由于我們沒有處理widget的index邏輯,導(dǎo)致在Column里不正常,后續(xù)我們再研究為什么,先來看下生命周期的回調(diào)

第一次運行

[圖片上傳失敗...(image-be90d-1600853501724)]

點擊按鈕

image

又發(fā)現(xiàn)一個問題,為什么我們的斷言沒生效呢?怎么又出現(xiàn)了firstBuild?哈哈,這里不要糾結(jié),由于TestWidget并非const,導(dǎo)致setState后,又重新被創(chuàng)建了,而對應(yīng)的Element也同樣是創(chuàng)建了新的值,最終導(dǎo)致被重新執(zhí)行。其實這個TestWidget已經(jīng)不是上一個了,那我們加入 const修飾再看看

/// 改成const
const TestWidget()

/// 加入當(dāng)前widget hashcode輸出,用來判斷兩次是否一致
  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement  widget hashcode${widget.hashCode}");
    print("LifecycleElement hashcode${this.hashCode}");
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
    assert(_child == null);
    print("LifecycleElement firstBuild");
    performRebuild();
  }

最終(啟動,點擊按鈕兩次的效果)運行效果如下:

image

兩次運行Widget保持一致,這就避免了Widget的重建

小結(jié)

經(jīng)過測試我們發(fā)現(xiàn):

  • Widget的創(chuàng)建可以做到復(fù)用,通過const修飾
  • Element并沒有復(fù)用,其實原因應(yīng)該是在于isShow為false的時候?qū)е缕浔籨eactivate 然后unmount,從Element樹種被移除掉。
  • 有的人肯定有些疑問,怎么全程沒看到activate呢?它不應(yīng)該屬于生命周期的一部分嗎?這個就需要用到Key了,在接下來的課程里,講到Key的時候,我們再詳細(xì)的學(xué)習(xí)。

總結(jié)

本期我們對Widget,Element有了一個詳細(xì)的認(rèn)知,但其實它還有一個State類(StatefulWidget的核心實現(xiàn))和RenderObject類,這兩個下期我再分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。