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)用了傳遞進來的Widget參數(shù)build函數(shù),那么我們就在TestWidget中添加函數(shù)鳞尔,并完善下邏輯后是這樣的

class TestWidget extends Widget {

  @override
  Element createElement() {
    /// 將自己傳遞進去嬉橙,讓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() {
    /// 將自己傳遞進去沼瘫,讓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斷言是防止后面再加載進來的時候多次觸發(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)系作者
  • 序言:七十年代末削彬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吃警,老刑警劉巖糕篇,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酌心,居然都是意外死亡,警方通過查閱死者的電腦和手機挑豌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門安券,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人氓英,你說我怎么就攤上這事侯勉。” “怎么了铝阐?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵址貌,是天一觀的道長。 經(jīng)常有香客問我徘键,道長练对,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任吹害,我火速辦了婚禮螟凭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘它呀。我一直安慰自己螺男,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布纵穿。 她就那樣靜靜地躺著下隧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谓媒。 梳的紋絲不亂的頭發(fā)上淆院,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音篙耗,去河邊找鬼迫筑。 笑死,一個胖子當(dāng)著我的面吹牛宗弯,可吹牛的內(nèi)容都是我干的脯燃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蒙保,長吁一口氣:“原來是場噩夢啊……” “哼辕棚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤逝嚎,失蹤者是張志新(化名)和其女友劉穎扁瓢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體补君,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡引几,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挽铁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伟桅。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叽掘,靈堂內(nèi)的尸體忽然破棺而出楣铁,到底是詐尸還是另有隱情,我是刑警寧澤更扁,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布盖腕,位于F島的核電站,受9級特大地震影響浓镜,放射性物質(zhì)發(fā)生泄漏溃列。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一竖哩、第九天 我趴在偏房一處隱蔽的房頂上張望哭廉。 院中可真熱鬧,春花似錦相叁、人聲如沸遵绰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椿访。三九已至,卻和暖如春虑润,著一層夾襖步出監(jiān)牢的瞬間成玫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工拳喻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哭当,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓冗澈,卻偏偏與公主長得像钦勘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亚亲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354