背景
為什么說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)
- 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-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)
可以看到笆环,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)吨铸,那么這么重要行拢,我們需要從哪方面了解呢?一個很重要的知識點就是生命周期诞吱,只有了解了正確的生命周期舟奠,你才能在合適的時間做合適的操作
為了驗證該圖,我們加入日志打印下房维,代碼如下:
/// 創(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;
});
},
運行一下項目查看日志
- 調(diào)用 element.mount(parentElement,newSlot)
- 調(diào)用 update(Widget newWidget)
- 調(diào)用 updateChild(Element child, Widget newWidget, newSlot)
然后我們點擊下按鈕
- 調(diào)用 deactivate()
- 調(diào)用 unmount()
我們再點擊下按鈕
這次只有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"以躯,主要是為了過濾垃圾日志,重新運行項目。如圖
點擊后按鈕
再點擊
發(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)]
點擊按鈕
又發(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();
}
最終(啟動饵史,點擊按鈕兩次的效果)運行效果如下:
兩次運行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類,這兩個下期我再分析迈嘹。