Flutter核心原理Widget(1)

概念

在前面的介紹中,我們知道在Flutter中幾乎所有的對象都是一個Widget。與原生開發(fā)中“控件”不同的是流部,F(xiàn)lutter中的Widget的概念更廣泛雕憔,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector widget廉赔、用于APP主題數(shù)據(jù)傳遞的Theme等等,而原生開發(fā)中的控件通常只是指UI元素。在后面的內(nèi)容中音念,我們在描述UI元素時可能會用到“控件”、“組件”這樣的概念躏敢,讀者心里需要知道他們就是widget闷愤,只是在不同場景的不同表述而已。由于Flutter主要就是用于構(gòu)建用戶界面的件余,所以讥脐,在大多數(shù)時候,讀者可以認(rèn)為widget就是一個控件啼器,不必糾結(jié)于概念旬渠。

Widget與Element

在Flutter中,Widget的功能是“描述一個UI元素的配置數(shù)據(jù)”端壳,它就是說告丢,Widget其實(shí)并不是表示最終繪制在設(shè)備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數(shù)據(jù)损谦。
實(shí)際上岖免,F(xiàn)lutter中真正代表屏幕上顯示元素的類是Element岳颇,也就是說Widget只是描述Element的配置數(shù)據(jù)!Widget只是UI元素的一個配置數(shù)據(jù)颅湘,并且一個Widget可以對應(yīng)多個Element话侧。這是因?yàn)橥粋€Widget對象可以被添加到UI樹的不同部分,而真正渲染時闯参,UI樹的每一個Element節(jié)點(diǎn)都會對應(yīng)一個Widget對象瞻鹏。總結(jié)一下:

  • Widget實(shí)際上就是Element的配置數(shù)據(jù)鹿寨,Widget樹實(shí)際上是一個配置樹新博,而真正的UI渲染樹是由Element構(gòu)成;不過脚草,由于Element是通過Widget生成的叭披,所以它們之間有對應(yīng)關(guān)系,在大多數(shù)場景玩讳,我們可以寬泛地認(rèn)為Widget樹就是指UI控件樹或UI渲染樹涩蜘。
  • 一個Widget對象可以對應(yīng)多個Element對象。這很好理解熏纯,根據(jù)同一份配置(Widget)同诫,可以創(chuàng)建多個實(shí)例(Element)。

Widget主要接口

我們先來看一下Widget類的聲明:

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

  • Widget類繼承自DiagnosticableTree樟澜,DiagnosticableTree即“診斷樹”误窖,主要作用是提供調(diào)試信息。
  • Key: 這個key屬性類似于React/Vue中的key秩贰,主要的作用是決定是否在下一次build時復(fù)用舊的widget霹俺,決定的條件在canUpdate()方法中。
  • createElement():正如前文所述“一個Widget可以對應(yīng)多個Element”毒费;Flutter Framework在構(gòu)建UI樹時丙唧,會先調(diào)用此方法生成對應(yīng)節(jié)點(diǎn)的Element對象。此方法是Flutter Framework隱式調(diào)用的觅玻,在我們開發(fā)過程中基本不會調(diào)用到想际。
  • debugFillProperties(...) 復(fù)寫父類的方法,主要是設(shè)置診斷樹的一些特性溪厘。
  • canUpdate(...)是一個靜態(tài)方法胡本,它主要用于在Widget樹重新build時復(fù)用舊的widget,其實(shí)具體來說畸悬,應(yīng)該是:是否用新的Widget對象去更新舊UI樹上所對應(yīng)的Element對象的配置侧甫;通過其源碼我們可以看到,只要newWidgetoldWidgetruntimeTypekey同時相等時就會用newWidget去更新Element對象的配置,否則就會創(chuàng)建新的Element披粟。

另外Widget類本身是一個抽象類彩扔,其中最核心的就是定義了createElement()接口,在Flutter開發(fā)中僻爽,我們一般都不用直接繼承Widget類來實(shí)現(xiàn)一個新組件,相反贾惦,我們通常會通過繼承StatelessWidgetStatefulWidget來間接繼承Widget類來實(shí)現(xiàn)胸梆。StatelessWidgetStatefulWidget都是直接繼承自Widget類,而這兩個類也正是Flutter中非常重要的兩個抽象類须板,它們引入了兩種Widget模型碰镜,接下來我們將重點(diǎn)介紹一下這兩個類。

StatelessWidget

StatelessWidget相對比較簡單习瑰,它繼承自Widget類绪颖,重寫了createElement()方法:

@override
StatelessElement createElement() => new StatelessElement(this);

StatelessElement 間接繼承自Element類,與StatelessWidget相對應(yīng)(作為其配置數(shù)據(jù))甜奄。

StatelessWidget用于不需要維護(hù)狀態(tài)的場景柠横,它通常在build方法中通過嵌套其它Widget來構(gòu)建UI,在構(gòu)建過程中會遞歸的構(gòu)建其嵌套的Widget课兄。我們看一個簡單的例子:

class Echo extends StatelessWidget {
  const Echo({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);

  final String text;
  final Color backgroundColor;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

上面的代碼牍氛,實(shí)現(xiàn)了一個回顯字符串的Echo widget。

按照慣例烟阐,widget的構(gòu)造函數(shù)參數(shù)應(yīng)使用命名參數(shù)搬俊,命名參數(shù)中的必要參數(shù)要添加@required標(biāo)注,這樣有利于靜態(tài)代碼分析器進(jìn)行檢查蜒茄。另外唉擂,在繼承widget時,第一個參數(shù)通常應(yīng)該是Key檀葛,另外玩祟,如果Widget需要接收子Widget,那么childchildren參數(shù)通常應(yīng)被放在參數(shù)列表的最后屿聋。同樣是按照慣例卵凑,Widget的屬性應(yīng)盡可能的被聲明為final,防止被意外改變胜臊。

然后我們可以通過如下方式使用它:

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

運(yùn)行后效果如圖

Context

build方法有一個context參數(shù)勺卢,它是BuildContext類的一個實(shí)例,表示當(dāng)前widget在widget樹中的上下文象对,每一個widget都會對應(yīng)一個context對象(因?yàn)槊恳粋€widget都是widget樹上的一個節(jié)點(diǎn))黑忱。實(shí)際上,context是當(dāng)前widget在widget樹中位置中執(zhí)行”相關(guān)操作“的一個句柄,比如它提供了從當(dāng)前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.ancestorWidgetOfExactType(Scaffold);
          // 直接返回 AppBar的title菇曲, 此處實(shí)際上是Text("Context測試")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

運(yùn)行后效果如圖

StatefulWidget

StatelessWidget一樣,StatefulWidget也是繼承自Widget類抚吠,并重寫了createElement()方法常潮,不同的是返回的Element 對象并不相同;另外StatefulWidget類中添加了一個新的接口createState()楷力。

下面我們看看StatefulWidget的類定義:

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}

  • StatefulElement 間接繼承自Element類喊式,與StatefulWidget相對應(yīng)(作為其配置數(shù)據(jù))。StatefulElement中可能會多次調(diào)用createState()來創(chuàng)建狀態(tài)(State)對象萧朝。

  • createState() 用于創(chuàng)建和Stateful widget相關(guān)的狀態(tài)岔留,它在Stateful widget的生命周期中可能會被多次調(diào)用。例如检柬,當(dāng)一個Stateful widget同時插入到widget樹的多個位置時献联,F(xiàn)lutter framework就會調(diào)用該方法為每一個位置生成一個獨(dú)立的State實(shí)例,其實(shí)何址,本質(zhì)上就是一個StatefulElement對應(yīng)一個State實(shí)例里逆。

State

一個StatefulWidget類會對應(yīng)一個State類,State表示與其對應(yīng)的StatefulWidget要維護(hù)的狀態(tài)用爪,State中的保存的狀態(tài)信息可以:

  1. 在widget 構(gòu)建時可以被同步讀取运悲。
  2. 在widget生命周期中可以被改變,當(dāng)State被改變時项钮,可以手動調(diào)用其setState()方法通知Flutter framework狀態(tài)發(fā)生改變班眯,F(xiàn)lutter framework在收到消息后,會重新調(diào)用其build方法重新構(gòu)建widget樹烁巫,從而達(dá)到更新UI的目的署隘。

State中有兩個常用屬性:

  1. widget,它表示與該State實(shí)例關(guān)聯(lián)的widget實(shí)例亚隙,由Flutter framework動態(tài)設(shè)置磁餐。注意,這種關(guān)聯(lián)并非永久的阿弃,因?yàn)樵趹?yīng)用生命周期中诊霹,UI樹上的某一個節(jié)點(diǎn)的widget實(shí)例在重新構(gòu)建時可能會變化,但State實(shí)例只會在第一次插入到樹中時被創(chuàng)建渣淳,當(dāng)在重新構(gòu)建時脾还,如果widget被修改了,F(xiàn)lutter framework會動態(tài)設(shè)置State.widget為新的widget實(shí)例入愧。

  2. context鄙漏。StatefulWidget對應(yīng)的BuildContext嗤谚,作用同StatelessWidget的BuildContext。

State生命周期

理解State的生命周期對flutter開發(fā)非常重要怔蚌,為了加深讀者印象巩步,本節(jié)我們通過一個實(shí)例來演示一下State的生命周期。在接下來的示例中桦踊,我們實(shí)現(xiàn)一個計(jì)數(shù)器widget椅野,點(diǎn)擊它可以使計(jì)數(shù)器加1,由于要保存計(jì)數(shù)器的數(shù)值狀態(tài)籍胯,所以我們應(yīng)繼承StatefulWidget竟闪,代碼如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

CounterWidget接收一個initValue整型參數(shù),它表示計(jì)數(shù)器的初始值芒炼。下面我們看一下State的代碼:

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化狀態(tài)  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //點(diǎn)擊后計(jì)數(shù)器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

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

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

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

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

}

接下來,我們創(chuàng)建一個新路由术徊,在新路由中本刽,我們只顯示一個CounterWidget

Widget build(BuildContext context) {
  return CounterWidget();
}   

我們運(yùn)行應(yīng)用并打開該路由頁面,在新路由頁打開后赠涮,屏幕中央就會出現(xiàn)一個數(shù)字0子寓,然后控制臺日志輸出:

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

可以看到,在StatefulWidget插入到Widget樹時首先initState方法會被調(diào)用笋除。

然后我們點(diǎn)擊??按鈕熱重載斜友,控制臺輸出日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

可以看到此時initStatedidChangeDependencies都沒有被調(diào)用,而此時didUpdateWidget被調(diào)用垃它。

接下來鲜屏,我們在widget樹中移除CounterWidget,將路由build方法改為:

Widget build(BuildContext context) {
  //移除計(jì)數(shù)器 
  //return CounterWidget();
  //隨便返回一個Text()
  return Text("xxx");
}

然后熱重載国拇,日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

我們可以看到洛史,在CounterWidget從widget樹中移除時,deactivedispose會依次被調(diào)用酱吝。

下面我們來看看各個回調(diào)函數(shù):

  • initState:當(dāng)Widget第一次插入到Widget樹時會被調(diào)用也殖,對于每一個State對象,F(xiàn)lutter framework只會調(diào)用一次該回調(diào)务热,所以忆嗜,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化崎岂、訂閱子樹的事件通知等捆毫。不能在該回調(diào)中調(diào)用BuildContext.inheritFromWidgetOfExactType(該方法用于在Widget樹上獲取離當(dāng)前widget最近的一個父級InheritFromWidget,關(guān)于InheritedWidget我們將在后面章節(jié)介紹)冲甘,原因是在初始化完成后冻璃,Widget樹中的InheritFromWidget也可能會發(fā)生變化响谓,所以正確的做法應(yīng)該在build()方法或didChangeDependencies()中調(diào)用它。

  • didChangeDependencies():當(dāng)State對象的依賴發(fā)生變化時會被調(diào)用省艳;例如:在之前build() 中包含了一個InheritedWidget娘纷,然后在之后的build()InheritedWidget發(fā)生了變化,那么此時InheritedWidget的子widget的didChangeDependencies()回調(diào)都會被調(diào)用跋炕。典型的場景是當(dāng)系統(tǒng)語言Locale或應(yīng)用主題改變時赖晶,F(xiàn)lutter framework會通知widget調(diào)用此回調(diào)。

  • build():此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了辐烂,它主要是用于構(gòu)建Widget子樹的遏插,會在如下場景被調(diào)用:

    1. 在調(diào)用initState()之后。
    2. 在調(diào)用didUpdateWidget()之后纠修。
    3. 在調(diào)用setState()之后胳嘲。
    4. 在調(diào)用didChangeDependencies()之后。
    5. 在State對象從樹中一個位置移除后(會調(diào)用deactivate)又重新插入到樹的其它位置之后扣草。
  • reassemble():此回調(diào)是專門為了開發(fā)調(diào)試而提供的了牛,在熱重載(hot reload)時會被調(diào)用,此回調(diào)在Release模式下永遠(yuǎn)不會被調(diào)用辰妙。

  • didUpdateWidget():在widget重新構(gòu)建時鹰祸,F(xiàn)lutter framework會調(diào)用Widget.canUpdate來檢測Widget樹中同一位置的新舊節(jié)點(diǎn),然后決定是否需要更新密浑,如果Widget.canUpdate返回true則會調(diào)用此回調(diào)蛙婴。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true尔破,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被調(diào)用街图。

  • deactivate():當(dāng)State對象從樹中被移除時,會調(diào)用此回調(diào)懒构。在一些場景下台夺,F(xiàn)lutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實(shí)現(xiàn))痴脾。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法颤介。

  • dispose():當(dāng)State對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源赞赖。

StatefulWidget生命周期如圖所示:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滚朵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子前域,更是在濱河造成了極大的恐慌辕近,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匿垄,死亡現(xiàn)場離奇詭異移宅,居然都是意外死亡归粉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門漏峰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糠悼,“玉大人,你說我怎么就攤上這事浅乔【笪梗” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵靖苇,是天一觀的道長席噩。 經(jīng)常有香客問我,道長贤壁,這世上最難降的妖魔是什么悼枢? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮脾拆,結(jié)果婚禮上馒索,老公的妹妹穿的比我還像新娘。我一直安慰自己假丧,他們只是感情好双揪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布动羽。 她就那樣靜靜地躺著包帚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪运吓。 梳的紋絲不亂的頭發(fā)上渴邦,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音拘哨,去河邊找鬼谋梭。 笑死,一個胖子當(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
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兼丰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唆缴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳍征。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖琐谤,靈堂內(nèi)的尸體忽然破棺而出蟆技,到底是詐尸還是另有隱情,我是刑警寧澤斗忌,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布质礼,位于F島的核電站,受9級特大地震影響织阳,放射性物質(zhì)發(fā)生泄漏眶蕉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一唧躲、第九天 我趴在偏房一處隱蔽的房頂上張望造挽。 院中可真熱鬧,春花似錦弄痹、人聲如沸饭入。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谐丢。三九已至,卻和暖如春蚓让,著一層夾襖步出監(jiān)牢的瞬間乾忱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工历极, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窄瘟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓趟卸,卻偏偏與公主長得像蹄葱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锄列,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354