Flutter實(shí)戰(zhàn) Widget簡(jiǎn)介

2021-03-06 17:28 更新

#3.1.1 概念

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

#3.1.2 Widget與Element

在 Flutter 中,Widget 的功能是“描述一個(gè) UI 元素的配置數(shù)據(jù)”,它就是說(shuō),Widget 其實(shí)并不是表示最終繪制在設(shè)備屏幕上的顯示元素,而它只是描述顯示元素的一個(gè)配置數(shù)據(jù)。

實(shí)際上,F(xiàn)lutter 中真正代表屏幕上顯示元素的類是Element,也就是說(shuō) Widget 只是描述Element的配置數(shù)據(jù)!有關(guān)Element的詳細(xì)介紹我們將在本書后面的高級(jí)部分深入介紹,現(xiàn)在,讀者只需要知道:Widget 只是 UI 元素的一個(gè)配置數(shù)據(jù),并且一個(gè) Widget 可以對(duì)應(yīng)多個(gè)Element。這是因?yàn)橥粋€(gè) Widget 對(duì)象可以被添加到 U 樹的不同部分,而真正渲染時(shí),UI樹的每一個(gè)Element節(jié)點(diǎn)都會(huì)對(duì)應(yīng)一個(gè) Widget 對(duì)象??偨Y(jié)一下:

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

讀者應(yīng)該將這兩點(diǎn)牢記在心中。

#3.1.3 Widget主要接口

我們先來(lái)看一下Widget類的聲明:

  1. @immutable
  2. abstract class Widget extends DiagnosticableTree {
  3. const Widget({ this.key });
  4. final Key key;
  5. @protected
  6. Element createElement();
  7. @override
  8. String toStringShort() {
  9. return key == null ? '$runtimeType' : '$runtimeType-$key';
  10. }
  11. @override
  12. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  13. super.debugFillProperties(properties);
  14. properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  15. }
  16. static bool canUpdate(Widget oldWidget, Widget newWidget) {
  17. return oldWidget.runtimeType == newWidget.runtimeType
  18. && oldWidget.key == newWidget.key;
  19. }
  20. }

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

有關(guān) Key 和 Widget 復(fù)用的細(xì)節(jié)將會(huì)在本書后面高級(jí)部分深入討論,讀者現(xiàn)在只需知道,為 Widget 顯式添加 key 的話可能(但不一定)會(huì)使UI在重新構(gòu)建時(shí)變的高效,讀者目前可以先忽略此參數(shù)。本書后面的示例中,只會(huì)在構(gòu)建列表項(xiàng) UI 時(shí)會(huì)顯式指定 Key。

另外Widget類本身是一個(gè)抽象類,其中最核心的就是定義了createElement()接口,在 Flutter 開(kāi)發(fā)中,我們一般都不用直接繼承Widget類來(lái)實(shí)現(xiàn)一個(gè)新組件,相反,我們通常會(huì)通過(guò)繼承StatelessWidgetStatefulWidget來(lái)間接繼承Widget類來(lái)實(shí)現(xiàn)。StatelessWidgetStatefulWidget都是直接繼承自Widget類,而這兩個(gè)類也正是 Flutter 中非常重要的兩個(gè)抽象類,它們引入了兩種 Widget 模型,接下來(lái)我們將重點(diǎn)介紹一下這兩個(gè)類。

#3.1.4 StatelessWidget

在之前的章節(jié)中,我們已經(jīng)簡(jiǎn)單介紹過(guò)StatelessWidgetStatelessWidget相對(duì)比較簡(jiǎn)單,它繼承自Widget類,重寫了createElement()方法:

  1. @override
  2. StatelessElement createElement() => new StatelessElement(this);

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

StatelessWidget用于不需要維護(hù)狀態(tài)的場(chǎng)景,它通常在build方法中通過(guò)嵌套其它 Widget 來(lái)構(gòu)建UI,在構(gòu)建過(guò)程中會(huì)遞歸的構(gòu)建其嵌套的 Widget。我們看一個(gè)簡(jiǎn)單的例子:

  1. class Echo extends StatelessWidget {
  2. const Echo({
  3. Key key,
  4. @required this.text,
  5. this.backgroundColor:Colors.grey,
  6. }):super(key:key);
  7. final String text;
  8. final Color backgroundColor;
  9. @override
  10. Widget build(BuildContext context) {
  11. return Center(
  12. child: Container(
  13. color: backgroundColor,
  14. child: Text(text),
  15. ),
  16. );
  17. }
  18. }

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

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

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

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

運(yùn)行后效果如圖3-1所示:

圖3-1

#Context

build方法有一個(gè)context參數(shù),它是BuildContext類的一個(gè)實(shí)例,表示當(dāng)前 widget 在 widget 樹中的上下文,每一個(gè) widget 都會(huì)對(duì)應(yīng)一個(gè) context 對(duì)象(因?yàn)槊恳粋€(gè) widget 都是 widget 樹上的一個(gè)節(jié)點(diǎn))。實(shí)際上,context是當(dāng)前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個(gè)句柄,比如它提供了從當(dāng)前 widget 開(kāi)始向上遍歷 widget 樹以及按照 widget 類型查找父級(jí) widget 的方法。下面是在子樹中獲取父級(jí) widget 的一個(gè)示例:

  1. class ContextRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. appBar: AppBar(
  6. title: Text("Context測(cè)試"),
  7. ),
  8. body: Container(
  9. child: Builder(builder: (context) {
  10. // 在Widget樹中向上查找最近的父級(jí)`Scaffold` widget
  11. Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
  12. // 直接返回 AppBar的title, 此處實(shí)際上是Text("Context測(cè)試")
  13. return (scaffold.appBar as AppBar).title;
  14. }),
  15. ),
  16. );
  17. }
  18. }

運(yùn)行后效果如圖3-1-1所示:

圖3-1-1

注意:對(duì)于BuildContext讀者現(xiàn)在可以先作了解,隨著本書后面內(nèi)容的展開(kāi),也會(huì)用到Context的一些方法,讀者可以通過(guò)具體的場(chǎng)景對(duì)其有個(gè)直觀的認(rèn)識(shí)。關(guān)于BuildContext更多的內(nèi)容,我們也將在后面高級(jí)部分再深入介紹。

#3.1.5 StatefulWidget

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

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

  1. abstract class StatefulWidget extends Widget {
  2. const StatefulWidget({ Key key }) : super(key: key);
  3. @override
  4. StatefulElement createElement() => new StatefulElement(this);
  5. @protected
  6. State createState();
  7. }

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

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

在本書中經(jīng)常會(huì)出現(xiàn)“樹”的概念,在不同的場(chǎng)景可能指不同的意思,在說(shuō)“widget 樹”時(shí)它可以指 widget 結(jié)構(gòu)樹,但由于 widget 與 Element 有對(duì)應(yīng)關(guān)系(一可能對(duì)多),在有些場(chǎng)景(Flutter 的 SDK 文檔中)也代指“UI 樹”的意思。而在 stateful widget 中,State 對(duì)象也和StatefulElement具有對(duì)應(yīng)關(guān)系(一對(duì)一),所以在 Flutter 的 SDK 文檔中,可以經(jīng)??吹健皬臉渲幸瞥?State 對(duì)象”或“插入 State 對(duì)象到樹中”這樣的描述。其實(shí),無(wú)論哪種描述,其意思都是在描述“一棵構(gòu)成用戶界面的節(jié)點(diǎn)元素的樹”,讀者不必糾結(jié)于這些概念,還是那句話“得其神,忘其形”,因此,本書中出現(xiàn)的各種“樹”,如果沒(méi)有特別說(shuō)明,讀者都可抽象的認(rèn)為它是“一棵構(gòu)成用戶界面的節(jié)點(diǎn)元素的樹”。

#3.1.6 State

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

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

State中有兩個(gè)常用屬性:

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

#State生命周期

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

  1. class CounterWidget extends StatefulWidget {
  2. const CounterWidget({
  3. Key key,
  4. this.initValue: 0
  5. });
  6. final int initValue;
  7. @override
  8. _CounterWidgetState createState() => new _CounterWidgetState();
  9. }

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

  1. class _CounterWidgetState extends State<CounterWidget> {
  2. int _counter;
  3. @override
  4. void initState() {
  5. super.initState();
  6. //初始化狀態(tài)
  7. _counter=widget.initValue;
  8. print("initState");
  9. }
  10. @override
  11. Widget build(BuildContext context) {
  12. print("build");
  13. return Scaffold(
  14. body: Center(
  15. child: FlatButton(
  16. child: Text('$_counter'),
  17. //點(diǎn)擊后計(jì)數(shù)器自增
  18. onPressed:()=>setState(()=> ++_counter,
  19. ),
  20. ),
  21. ),
  22. );
  23. }
  24. @override
  25. void didUpdateWidget(CounterWidget oldWidget) {
  26. super.didUpdateWidget(oldWidget);
  27. print("didUpdateWidget");
  28. }
  29. @override
  30. void deactivate() {
  31. super.deactivate();
  32. print("deactive");
  33. }
  34. @override
  35. void dispose() {
  36. super.dispose();
  37. print("dispose");
  38. }
  39. @override
  40. void reassemble() {
  41. super.reassemble();
  42. print("reassemble");
  43. }
  44. @override
  45. void didChangeDependencies() {
  46. super.didChangeDependencies();
  47. print("didChangeDependencies");
  48. }
  49. }

接下來(lái),我們創(chuàng)建一個(gè)新路由,在新路由中,我們只顯示一個(gè)CounterWidget

  1. Widget build(BuildContext context) {
  2. return CounterWidget();
  3. }

我們運(yùn)行應(yīng)用并打開(kāi)該路由頁(yè)面,在新路由頁(yè)打開(kāi)后,屏幕中央就會(huì)出現(xiàn)一個(gè)數(shù)字0,然后控制臺(tái)日志輸出:

  1. I/flutter ( 5436): initState
  2. I/flutter ( 5436): didChangeDependencies
  3. I/flutter ( 5436): build

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

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

  1. I/flutter ( 5436): reassemble
  2. I/flutter ( 5436): didUpdateWidget
  3. I/flutter ( 5436): build

可以看到此時(shí)initStatedidChangeDependencies都沒(méi)有被調(diào)用,而此時(shí)didUpdateWidget被調(diào)用。

接下來(lái),我們?cè)?widget 樹中移除CounterWidget,將路由build方法改為:

  1. Widget build(BuildContext context) {
  2. //移除計(jì)數(shù)器
  3. //return CounterWidget();
  4. //隨便返回一個(gè)Text()
  5. return Text("xxx");
  6. }

然后熱重載,日志如下:

  1. I/flutter ( 5436): reassemble
  2. I/flutter ( 5436): deactive
  3. I/flutter ( 5436): dispose

我們可以看到,在CounterWidget從 widget 樹中移除時(shí),deactivedispose會(huì)依次被調(diào)用。

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

  • initState:當(dāng) Widget 第一次插入到 Widget 樹時(shí)會(huì)被調(diào)用,對(duì)于每一個(gè) State 對(duì)象,F(xiàn)lutter framework 只會(huì)調(diào)用一次該回調(diào),所以,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化、訂閱子樹的事件通知等。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType(該方法用于在 Widget 樹上獲取離當(dāng)前 widget 最近的一個(gè)父級(jí)InheritFromWidget,關(guān)于InheritedWidget我們將在后面章節(jié)介紹),原因是在初始化完成后,Widget 樹中的InheritFromWidget也可能會(huì)發(fā)生變化,所以正確的做法應(yīng)該在在build()方法或didChangeDependencies()中調(diào)用它。
  • didChangeDependencies():當(dāng) State 對(duì)象的依賴發(fā)生變化時(shí)會(huì)被調(diào)用;例如:在之前build() 中包含了一個(gè)InheritedWidget,然后在之后的build()InheritedWidget發(fā)生了變化,那么此時(shí)InheritedWidget的子 widget 的didChangeDependencies()回調(diào)都會(huì)被調(diào)用。典型的場(chǎng)景是當(dāng)系統(tǒng)語(yǔ)言 Locale 或應(yīng)用主題改變時(shí),F(xiàn)lutter framework 會(huì)通知 widget 調(diào)用此回調(diào)。
  • build():此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了,它主要是用于構(gòu)建 Widget 子樹的,會(huì)在如下場(chǎng)景被調(diào)用:
    1. 在調(diào)用initState()之后。
    2. 在調(diào)用didUpdateWidget()之后。
    3. 在調(diào)用setState()之后。
    4. 在調(diào)用didChangeDependencies()之后。
    5. 在 State 對(duì)象從樹中一個(gè)位置移除后(會(huì)調(diào)用deactivate)又重新插入到樹的其它位置之后。
  • reassemble():此回調(diào)是專門為了開(kāi)發(fā)調(diào)試而提供的,在熱重載(hot reload)時(shí)會(huì)被調(diào)用,此回調(diào)在Release模式下永遠(yuǎn)不會(huì)被調(diào)用。
  • didUpdateWidget():在 widget 重新構(gòu)建時(shí),F(xiàn)lutter framework 會(huì)調(diào)用Widget.canUpdate來(lái)檢測(cè) Widget 樹中同一位置的新舊節(jié)點(diǎn),然后決定是否需要更新,如果Widget.canUpdate返回true則會(huì)調(diào)用此回調(diào)。正如之前所述,Widget.canUpdate會(huì)在新舊widget的key和runtimeType 同時(shí)相等時(shí)會(huì)返回true,也就是說(shuō)在在新舊 widget 的 key 和 runtimeType 同時(shí)相等時(shí)didUpdateWidget()就會(huì)被調(diào)用。
  • deactivate():當(dāng) State 對(duì)象從樹中被移除時(shí),會(huì)調(diào)用此回調(diào)。在一些場(chǎng)景下,F(xiàn)lutter framework 會(huì)將 State 對(duì)象重新插到樹中,如包含此 State 對(duì)象的子樹在樹的一個(gè)位置移動(dòng)到另一個(gè)位置時(shí)(可以通過(guò) GlobalKey 來(lái)實(shí)現(xiàn))。如果移除后沒(méi)有重新插入到樹中則緊接著會(huì)調(diào)用dispose()方法。
  • dispose():當(dāng) State 對(duì)象從樹中被永久移除時(shí)調(diào)用;通常在此回調(diào)中釋放資源。

StatefulWidget 生命周期如圖3-2所示:

圖3-2

注意:在繼承StatefulWidget重寫其方法時(shí),對(duì)于包含@mustCallSuper標(biāo)注的父類方法,都要在子類方法中先調(diào)用父類方法。

#為什么要將build方法放在State中,而不是放在StatefulWidget中?

現(xiàn)在,我們回答之前提出的問(wèn)題,為什么build()方法放在 State(而不是StatefulWidget)中 ?這主要是為了提高開(kāi)發(fā)的靈活性。如果將build()方法在StatefulWidget中則會(huì)有兩個(gè)問(wèn)題:

  • 狀態(tài)訪問(wèn)不便。

試想一下,如果我們的StatefulWidget有很多狀態(tài),而每次狀態(tài)改變都要調(diào)用build方法,由于狀態(tài)是保存在 State 中的,如果build方法在StatefulWidget中,那么build方法和狀態(tài)分別在兩個(gè)類中,那么構(gòu)建時(shí)讀取狀態(tài)將會(huì)很不方便!試想一下,如果真的將build方法放在 StatefulWidget 中的話,由于構(gòu)建用戶界面過(guò)程需要依賴 State,所以build方法將必須加一個(gè)State參數(shù),大概是下面這樣:

  1. Widget build(BuildContext context, State state){
  2. //state.counter
  3. ...
  4. }

這樣的話就只能將 State 的所有狀態(tài)聲明為公開(kāi)的狀態(tài),這樣才能在 State 類外部訪問(wèn)狀態(tài)!但是,將狀態(tài)設(shè)置為公開(kāi)后,狀態(tài)將不再具有私密性,這就會(huì)導(dǎo)致對(duì)狀態(tài)的修改將會(huì)變的不可控。但如果將build()方法放在 State 中的話,構(gòu)建過(guò)程不僅可以直接訪問(wèn)狀態(tài),而且也無(wú)需公開(kāi)私有狀態(tài),這會(huì)非常方便。

  • 繼承StatefulWidget不便。

例如,F(xiàn)lutter 中有一個(gè)動(dòng)畫 widget 的基類AnimatedWidget,它繼承自StatefulWidget類。AnimatedWidget中引入了一個(gè)抽象方法build(BuildContext context),繼承自AnimatedWidget的動(dòng)畫widget都要實(shí)現(xiàn)這個(gè)build方法?,F(xiàn)在設(shè)想一下,如果StatefulWidget 類中已經(jīng)有了一個(gè)build方法,正如上面所述,此時(shí)build方法需要接收一個(gè)state對(duì)象,這就意味著AnimatedWidget必須將自己的 State 對(duì)象(記為_(kāi)animatedWidgetState)提供給其子類,因?yàn)樽宇愋枰谄?code>build方法中調(diào)用父類的build方法,代碼可能如下:

  1. class MyAnimationWidget extends AnimatedWidget{
  2. @override
  3. Widget build(BuildContext context, State state){
  4. //由于子類要用到AnimatedWidget的狀態(tài)對(duì)象_animatedWidgetState,
  5. //所以AnimatedWidget必須通過(guò)某種方式將其狀態(tài)對(duì)象_animatedWidgetState
  6. //暴露給其子類
  7. super.build(context, _animatedWidgetState)
  8. }
  9. }

這樣很顯然是不合理的,因?yàn)?/p>

  1. AnimatedWidget的狀態(tài)對(duì)象是AnimatedWidget內(nèi)部實(shí)現(xiàn)細(xì)節(jié),不應(yīng)該暴露給外部。
  2. 如果要將父類狀態(tài)暴露給子類,那么必須得有一種傳遞機(jī)制,而做這一套傳遞機(jī)制是無(wú)意義的,因?yàn)楦缸宇愔g狀態(tài)的傳遞和子類本身邏輯是無(wú)關(guān)的。

綜上所述,可以發(fā)現(xiàn),對(duì)于StatefulWidget,將build方法放在State中,可以給開(kāi)發(fā)帶來(lái)很大的靈活性。

#3.1.7 在Widget樹中獲取State對(duì)象

由于 StatefulWidget 的的具體邏輯都在其 State 中,所以很多時(shí)候,我們需要獲取 StatefulWidget 對(duì)應(yīng)的 State 對(duì)象來(lái)調(diào)用一些方法,比如Scaffold組件對(duì)應(yīng)的狀態(tài)類ScaffoldState中就定義了打開(kāi) SnackBar(路由頁(yè)底部提示條)的方法。我們有兩種方法在子 widget 樹中獲取父級(jí) StatefulWidget 的 State 對(duì)象。

#通過(guò)Context獲取

context對(duì)象有一個(gè)findAncestorStateOfType()方法,該方法可以從當(dāng)前節(jié)點(diǎn)沿著 widget 樹向上查找指定類型的 StatefulWidget 對(duì)應(yīng)的 State 對(duì)象。下面是實(shí)現(xiàn)打開(kāi) SnackBar 的示例:

  1. Scaffold(
  2. appBar: AppBar(
  3. title: Text("子樹中獲取State對(duì)象"),
  4. ),
  5. body: Center(
  6. child: Builder(builder: (context) {
  7. return RaisedButton(
  8. onPressed: () {
  9. // 查找父級(jí)最近的Scaffold對(duì)應(yīng)的ScaffoldState對(duì)象
  10. ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
  11. //調(diào)用ScaffoldState的showSnackBar來(lái)彈出SnackBar
  12. _state.showSnackBar(
  13. SnackBar(
  14. content: Text("我是SnackBar"),
  15. ),
  16. );
  17. },
  18. child: Text("顯示SnackBar"),
  19. );
  20. }),
  21. ),
  22. );

上面示例運(yùn)行后,點(diǎn)擊”顯示 SnackBar“,效果如圖3-1-2所示:

圖3-1-2

一般來(lái)說(shuō),如果 StatefulWidget 的狀態(tài)是私有的(不應(yīng)該向外部暴露),那么我們代碼中就不應(yīng)該去直接獲取其 State 對(duì)象;如果 StatefulWidget 的狀態(tài)是希望暴露出的(通常還有一些組件的操作方法),我們則可以去直接獲取其 State 對(duì)象。但是通過(guò)context.findAncestorStateOfType獲取 StatefulWidget 的狀態(tài)的方法是通用的,我們并不能在語(yǔ)法層面指定 StatefulWidget 的狀態(tài)是否私有,所以在 Flutter 開(kāi)發(fā)中便有了一個(gè)默認(rèn)的約定:如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當(dāng)在 StatefulWidget 中提供一個(gè)of靜態(tài)方法來(lái)獲取其 State 對(duì)象,開(kāi)發(fā)者便可直接通過(guò)該方法來(lái)獲??;如果 State 不希望暴露,則不提供of方法。這個(gè)約定在 Flutter SDK 里隨處可見(jiàn)。所以,上面示例中的Scaffold也提供了一個(gè)of方法,我們其實(shí)是可以直接調(diào)用它的:

  1. ...//省略無(wú)關(guān)代碼
  2. // 直接通過(guò)of靜態(tài)方法來(lái)獲取ScaffoldState
  3. ScaffoldState _state=Scaffold.of(context);
  4. _state.showSnackBar(
  5. SnackBar(
  6. content: Text("我是SnackBar"),
  7. ),
  8. );

#通過(guò)GlobalKey

Flutter 還有一種通用的獲取State對(duì)象的方法——通過(guò) GlobalKey 來(lái)獲取! 步驟分兩步:

  1. 給目標(biāo)StatefulWidget添加GlobalKey

  1. //定義一個(gè)globalKey, 由于GlobalKey要保持全局唯一性,我們使用靜態(tài)變量存儲(chǔ)
  2. static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
  3. ...
  4. Scaffold(
  5. key: _globalKey , //設(shè)置key
  6. ...
  7. )

  1. 通過(guò)GlobalKey來(lái)獲取State對(duì)象

  1. _globalKey.currentState.openDrawer()

GlobalKe y是 Flutter 提供的一種在整個(gè) APP 中引用 element 的機(jī)制。如果一個(gè) widget 設(shè)置了GlobalKey,那么我們便可以通過(guò)globalKey.currentWidget獲得該 widget對(duì)象、globalKey.currentElement來(lái)獲得widget對(duì)應(yīng)的element對(duì)象,如果當(dāng)前 widget 是StatefulWidget,則可以通過(guò)globalKey.currentState來(lái)獲得該 widget 對(duì)應(yīng)的 state 對(duì)象。

注意:使用 GlobalKey 開(kāi)銷較大,如果有其他可選方案,應(yīng)盡量避免使用它。另外同一個(gè)GlobalKey 在整個(gè) widget 樹中必須是唯一的,不能重復(fù)。

#3.1.8 Flutter SDK內(nèi)置組件庫(kù)介紹

Flutter 提供了一套豐富、強(qiáng)大的基礎(chǔ)組件,在基礎(chǔ)組件庫(kù)之上 Flutter 又提供了一套 Material 風(fēng)格(Android 默認(rèn)的視覺(jué)風(fēng)格)和一套 Cupertino 風(fēng)格(iOS 視覺(jué)風(fēng)格)的組件庫(kù)。要使用基礎(chǔ)組件庫(kù),需要先導(dǎo)入:

  1. import 'package:flutter/widgets.dart';

下面我們介紹一下常用的組件。

#基礎(chǔ)組件

  • Text (opens new window):該組件可讓您創(chuàng)建一個(gè)帶格式的文本。
  • Row (opens new window)、 Column (opens new window): 這些具有彈性空間的布局類 Widget 可讓您在水平(Row)和垂直(Column)方向上創(chuàng)建靈活的布局。其設(shè)計(jì)是基于Web開(kāi)發(fā)中的 Flexbox 布局模型。
  • Stack (opens new window): 取代線性布局 (譯者語(yǔ):和 Android 中的FrameLayout相似),Stack (opens new window)允許子 widget 堆疊, 你可以使用 Positioned (opens new window)來(lái)定位他們相對(duì)于Stack的上下左右四條邊的位置。Stacks 是基于 Web 開(kāi)發(fā)中的絕對(duì)定位(absolute positioning )布局模型設(shè)計(jì)的。
  • Container (opens new window): Container (opens new window)可讓您創(chuàng)建矩形視覺(jué)元素。container 可以裝飾一個(gè)BoxDecoration (opens new window), 如 background、一個(gè)邊框、或者一個(gè)陰影。 Container (opens new window)也可以具有邊距(margins)、填充(padding)和應(yīng)用于其大小的約束(constraints)。另外, Container (opens new window)可以使用矩陣在三維空間中對(duì)其進(jìn)行變換。

#Material組件

Flutter 提供了一套豐富的 Material 組件,它可以幫助我們構(gòu)建遵循 Material Design 設(shè)計(jì)規(guī)范的應(yīng)用程序。Material 應(yīng)用程序以MaterialApp (opens new window) 組件開(kāi)始, 該組件在應(yīng)用程序的根部創(chuàng)建了一些必要的組件,比如Theme組件,它用于配置應(yīng)用的主題。 是否使用MaterialApp (opens new window)完全是可選的,但是使用它是一個(gè)很好的做法。在之前的示例中,我們已經(jīng)使用過(guò)多個(gè) Material 組件了,如:Scaffold、AppBarFlatButton等。要使用 Material 組件,需要先引入它:

  1. import 'package:flutter/material.dart';

#Cupertino 組件

Flutter 也提供了一套豐富的 Cupertino 風(fēng)格的組件,盡管目前還沒(méi)有 Material 組件那么豐富,但是它仍在不斷的完善中。值得一提的是在 Material 組件庫(kù)中有一些組件可以根據(jù)實(shí)際運(yùn)行平臺(tái)來(lái)切換表現(xiàn)風(fēng)格,比如MaterialPageRoute,在路由切換時(shí),如果是 Android 系統(tǒng),它將會(huì)使用 Android 系統(tǒng)默認(rèn)的頁(yè)面切換動(dòng)畫(從底向上);如果是 iOS 系統(tǒng),它會(huì)使用 iOS 系統(tǒng)默認(rèn)的頁(yè)面切換動(dòng)畫(從右向左)。由于在前面的示例中還沒(méi)有 Cupertino 組件的示例,下面我們實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Cupertino 組件風(fēng)格的頁(yè)面:

  1. //導(dǎo)入cupertino widget庫(kù)
  2. import 'package:flutter/cupertino.dart';
  3. class CupertinoTestRoute extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. return CupertinoPageScaffold(
  7. navigationBar: CupertinoNavigationBar(
  8. middle: Text("Cupertino Demo"),
  9. ),
  10. child: Center(
  11. child: CupertinoButton(
  12. color: CupertinoColors.activeBlue,
  13. child: Text("Press"),
  14. onPressed: () {}
  15. ),
  16. ),
  17. );
  18. }
  19. }

下面(圖3-3)是在 iPhoneX 上頁(yè)面效果截圖:

圖3-3

#關(guān)于示例

本章后面章節(jié)的示例中會(huì)使用一些布局類組件,如Scaffold、Row、Column等,這些組件將在后面“布局類組件”一章中詳細(xì)介紹,讀者可以先不用關(guān)注。

#總結(jié)

Flutter 提供了豐富的組件,在實(shí)際的開(kāi)發(fā)中你可以根據(jù)需要隨意使用它們,而不必?fù)?dān)心引入過(guò)多組件庫(kù)會(huì)讓你的應(yīng)用安裝包變大,這不是 web 開(kāi)發(fā),dart 在編譯時(shí)只會(huì)編譯你使用了的代碼。由于 Material 和 Cupertino 都是在基礎(chǔ)組件庫(kù)之上的,所以如果我們的應(yīng)用中引入了這兩者之一,則不需要再引入flutter/widgets.dart了,因?yàn)樗鼈儍?nèi)部已經(jīng)引入過(guò)了。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)