Flutter 之 Widget 簡介 (八十七)

1. Widget 概念

在Flutter中幾乎所有的對象都是一個 widget 。與原生開發(fā)中“控件”不同的是,F(xiàn)lutter 中的 widget 的概念更廣泛,它不僅可以表示UI元素华烟,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector 、用于APP主題數(shù)據(jù)傳遞的 Theme 等等持灰,而原生開發(fā)中的控件通常只是指UI元素盔夜。

Flutter 中是通過 Widget 嵌套 Widget 的方式來構(gòu)建UI和進(jìn)行實踐處理的,所以記住堤魁,F(xiàn)lutter 中萬物皆為Widget喂链。

2. Widget 接口

在 Flutter 中, widget 的功能是“描述一個UI元素的配置信息”妥泉,它就是說衩藤, Widget 其實并不是表示最終繪制在設(shè)備屏幕上的顯示元素,所謂的配置信息就是 Widget 接收的參數(shù)涛漂,比如對于 Text 來講赏表,文本的內(nèi)容、對齊方式匈仗、文本樣式都是它的配置信息瓢剿。下面我們先來看一下 Widget 類的聲明:


@immutable // 不可變的
abstract class Widget extends DiagnosticableTree {

  const Widget({ this.key });

  final Key? key;

  @protected
  @factory
  Element createElement();

  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

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

  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;

  @override
  @nonVirtual
  int get hashCode => super.hashCode;

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

  ...
}
  • @immutable 代表 Widget 是不可變的,這會限制 Widget 中定義的屬性(即配置信息)必須是不可變的(final)悠轩,為什么不允許 Widget 中定義的屬性變化呢间狂?這是因為,F(xiàn)lutter 中如果屬性發(fā)生變化則會重新構(gòu)建Widget樹火架,即重新創(chuàng)建新的 Widget 實例來替換舊的 Widget 實例鉴象,所以允許 Widget 的屬性變化是沒有意義的,因為一旦 Widget 自己的屬性變了自己就會被替換何鸡。這也是為什么 Widget 中定義的屬性必須是 final 的原因纺弊。

  • widget類繼承自DiagnosticableTreeDiagnosticableTree即“診斷樹”骡男,主要作用是提供調(diào)試信息淆游。

  • Key: 這個key屬性類似于 React/Vue 中的key,主要的作用是決定是否在下一次build時復(fù)用舊的 widget 隔盛,決定的條件在canUpdate()方法中犹菱。

  • createElement():正如前文所述“一個 widget 可以對應(yīng)多個Element”;Flutter 框架在構(gòu)建UI樹時吮炕,會先調(diào)用此方法生成對應(yīng)節(jié)點的Element對象腊脱。此方法是 Flutter 框架隱式調(diào)用的,在我們開發(fā)過程中基本不會調(diào)用到龙亲。

  • debugFillProperties(...) 復(fù)寫父類的方法陕凹,主要是設(shè)置診斷樹的一些特性震鹉。

  • canUpdate(...)是一個靜態(tài)方法,它主要用于在 widget 樹重新build時復(fù)用舊的 widget 捆姜,其實具體來說传趾,應(yīng)該是:是否用新的 widget 對象去更新舊UI樹上所對應(yīng)的Element對象的配置;通過其源碼我們可以看到泥技,只要newWidget與oldWidget的runtimeType和key同時相等時就會用new widget去更新Element對象的配置浆兰,否則就會創(chuàng)建新的Element。

另外Widget類本身是一個抽象類珊豹,其中最核心的就是定義了createElement()接口簸呈,在 Flutter 開發(fā)中,我們一般都不用直接繼承Widget類來實現(xiàn)一個新組件店茶,相反蜕便,我們通常會通過繼承StatelessWidgetStatefulWidget來間接繼承widget類來實現(xiàn)。StatelessWidgetStatefulWidget都是直接繼承自Widget類贩幻,而這兩個類也正是 Flutter 中非常重要的兩個抽象類轿腺,它們引入了兩種 widget 模型,接下來我們將重點介紹一下這兩個類丛楚。

3. Flutter中的四棵樹

既然 Widget 只是描述一個UI元素的配置信息族壳,那么真正的布局、繪制是由誰來完成的呢趣些?Flutter 框架的的處理流程是這樣的:

  • 1.根據(jù) Widget 樹生成一個 Element 樹仿荆,Element 樹中的節(jié)點都繼承自 Element 類。
  • 2.根據(jù) Element 樹生成 Render 樹(渲染樹)坏平,渲染樹中的節(jié)點都繼承自RenderObject 類拢操。
  • 3.根據(jù)渲染樹生成 Layer 樹,然后上屏顯示舶替,Layer 樹中的節(jié)點都繼承自 Layer 類令境。

真正的布局和渲染邏輯在 Render 樹中,Element 是 Widget 和 RenderObject 的粘合劑坎穿,可以理解為一個中間代理展父。

我們通過一個例子來說明,假設(shè)有如下 Widget 樹:

Container( // 一個容器 widget
  color: Colors.blue, // 設(shè)置容器背景色
  child: Row( // 可以將子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 顯示圖片的 widget
      const Text('A'),
    ],
  ),
);

注意玲昧,如果 Container 設(shè)置了背景色,Container 內(nèi)部會創(chuàng)建一個新的 ColoredBox 來填充背景篮绿,相關(guān)邏輯如下:

if (color != null)
  current = ColoredBox(color: color!, child: current);

而 Image 內(nèi)部會通過 RawImage 來渲染圖片孵延、Text 內(nèi)部會通過 RichText 來渲染文本,所以最終的 Widget樹亲配、Element 樹尘应、渲染樹結(jié)構(gòu)下圖所示:

image.png

這里需要注意:

  1. 三棵樹中惶凝,Widget 和 Element 是一一對應(yīng)的,但并不和 RenderObject 一一對應(yīng)犬钢。比如 StatelessWidgetStatefulWidget 都沒有對應(yīng)的 RenderObject苍鲜。
  2. 渲染樹在上屏前會生成一棵 Layer 樹

4. StatelessWidget

4.1 簡介

在之前的章節(jié)中,我們已經(jīng)簡單介紹過StatelessWidget玷犹,StatelessWidget相對比較簡單混滔,它繼承自widget類,重寫了createElement()方法:

@override
StatelessElement createElement() => 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, //默認(rèn)為灰色
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

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

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

按照慣例吠昭,widget 的構(gòu)造函數(shù)參數(shù)應(yīng)使用命名參數(shù),命名參數(shù)中的必需要傳的參數(shù)要添加required關(guān)鍵字胧瓜,這樣有利于靜態(tài)代碼分析器進(jìn)行檢查怎诫;在繼承 widget 時,第一個參數(shù)通常應(yīng)該是Key贷痪。另外幻妓,如果 widget 需要接收子 widget ,那么child或children參數(shù)通常應(yīng)被放在參數(shù)列表的最后劫拢。同樣是按照慣例肉津, widget 的屬性應(yīng)盡可能的被聲明為final,防止被意外改變舱沧。

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

 Widget build(BuildContext context) {
  return Echo(text: "Hello World");
}
image.png

4.2 Context

build方法有一個context參數(shù)妹沙,它是BuildContext類的一個實例,表示當(dāng)前 widget 在 widget 樹中的上下文熟吏,每一個 widget 都會對應(yīng)一個 context 對象(因為每一個 widget 都是 widget 樹上的一個節(jié)點)距糖。實際上,context是當(dāng)前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個句柄(handle)牵寺,比如它提供了從當(dāng)前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法悍引。

下面是在子樹中獲取父級 widget 的一個示例:


class MSContextDemo extends StatelessWidget {
  const MSContextDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("ContextDemo")),
      body: Container(
        alignment: Alignment.center,
        child: Builder(builder: (context) {
          // 在 widget 樹中向上查找最近的父級`Scaffold`  widget
          Scaffold scaffold =
              context.findAncestorWidgetOfExactType<Scaffold>()!;
          // 直接返回 AppBar的title,此處實際上是Text("ContextDemo")
          return (scaffold.appBar as AppBar).title!;
        }),
      ),
    );
  }
}

image.png

5. StatefulWidget

和StatelessWidget一樣,StatefulWidget也是繼承自widget類帽氓,并重寫了createElement()方法趣斤,不同的是返回的Element 對象并不相同;另外StatefulWidget類中添加了一個新的接口createState()黎休。

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

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => StatefulElement(this);
    
  @protected
  State createState();
}
  • StatefulElement 間接繼承自Element類浓领,與StatefulWidget相對應(yīng)(作為其配置數(shù)據(jù))玉凯。StatefulElement中可能會多次調(diào)用createState()來創(chuàng)建狀態(tài)(State)對象。

  • createState() 用于創(chuàng)建和 StatefulWidget 相關(guān)的狀態(tài)联贩,它在StatefulWidget 的生命周期中可能會被多次調(diào)用漫仆。例如,當(dāng)一個 StatefulWidget 同時插入到 widget 樹的多個位置時泪幌,F(xiàn)lutter 框架就會調(diào)用該方法為每一個位置生成一個獨立的State實例盲厌,其實,本質(zhì)上就是一個StatefulElement對應(yīng)一個State實例座菠。

而在StatefulWidget 中狸眼,State 對象和StatefulElement具有一一對應(yīng)的關(guān)系,所以在Flutter的SDK文檔中浴滴,可以經(jīng)惩孛龋看到“從樹中移除 State 對象”或“插入 State 對象到樹中”這樣的描述,此時的樹指通過 widget 樹生成的 Element 樹升略。Flutter 的 SDK 文檔中經(jīng)常會提到“樹” 微王,我們可以根據(jù)語境來判斷到底指的是哪棵樹。其實品嚣,無論是哪棵樹炕倘,最終的目標(biāo)都是為了描述 UI 的結(jié)構(gòu)和繪制信息,所以在 Flutter 中遇到“樹”的概念時翰撑,若無特別說明罩旋,我們都可以理解為 “一棵構(gòu)成用戶界面的節(jié)點樹”

6. State

6.1 簡介

一個 StatefulWidget 類會對應(yīng)一個 State 類,State表示與其對應(yīng)的 StatefulWidget 要維護(hù)的狀態(tài)眶诈。

State 中的保存的狀態(tài)信息可以:

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

State 中有兩個常用屬性:

    1. widget,它表示與該 State 實例關(guān)聯(lián)的 widget 實例狡相,由Flutter 框架動態(tài)設(shè)置梯轻。注意,這種關(guān)聯(lián)并非永久的谣光,因為在應(yīng)用生命周期中檩淋,UI樹上的某一個節(jié)點的 widget 實例在重新構(gòu)建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建萄金,當(dāng)在重新構(gòu)建時蟀悦,如果 widget 被修改了,F(xiàn)lutter 框架會動態(tài)設(shè)置State. widget 為新的 widget 實例氧敢。
    1. context日戈。StatefulWidget對應(yīng)的 BuildContext,作用同StatelessWidget 的BuildContext孙乖。

6.2 State生命周期

我們以計數(shù)器功能為例浙炼,實現(xiàn)一個計數(shù)器 MSCounterWidget 組件 ,點擊它可以使計數(shù)器加1唯袄,由于要保存計數(shù)器的數(shù)值狀態(tài)弯屈,所以我們應(yīng)繼承StatefulWidget。
代碼如下:

class MSCounterWidget extends StatefulWidget {
  const MSCounterWidget({Key? key, this.initValue = 0}) : super(key: key);

  final int initValue;

  @override
  State<MSCounterWidget> createState() => _MSCounterWidgetState();
}

MSCounterWidget接收一個initValue整型參數(shù)恋拷,它表示計數(shù)器的初始值资厉。下面我們看一下State的代碼:


class _MSCounterWidgetState extends State<MSCounterWidget> {
  int _counter = 0;
  @override
  void initState() {
    //初始化狀態(tài)
    _counter = widget.initValue;
    super.initState();
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      appBar: AppBar(title: Text("CounterWidget")),
      body: Center(
        child: TextButton(
          child: Text("$_counter"),
          onPressed: () {
            _counter++;
            setState(() {});
          },
        ),
      ),
    );
  }

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

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

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

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

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

接下來,我們創(chuàng)建一個新路由蔬顾,在新路由中宴偿,我們只顯示一個MSCounterWidget:

class StateLifecycleTest extends StatelessWidget {
  const StateLifecycleTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MSCounterWidget();
  }
}

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

flutter: initState
flutter: didChangeDependencies
flutter: build

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

然后我們點擊??按鈕熱重載娩践,控制臺輸出日志如下:

flutter: reassemble
flutter: didUpdateWidget
flutter: build

可以看到此時initState 和didChangeDependencies都沒有被調(diào)用,而此時didUpdateWidget被調(diào)用烹骨。

接下來翻伺,我們在 widget 樹中移除MSCounterWidget,將 StateLifecycleTest 的 build方法改為:

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

然后熱重載展氓,日志如下:

flutter: reassemble
flutter: deactivate
flutter: dispose

我們可以看到穆趴,在MSCounterWidget從 widget 樹中移除時,deactive和dispose會依次被調(diào)用遇汞。

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

  • initState:當(dāng) widget 第一次插入到 widget 樹時會被調(diào)用未妹,對于每一個State對象,F(xiàn)lutter 框架只會調(diào)用一次該回調(diào)空入,所以络它,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化歪赢、訂閱子樹的事件通知等化戳。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType(該方法用于在 widget 樹上獲取離當(dāng)前 widget 最近的一個父級InheritedWidget),原因是在初始化完成后, widget 樹中的InheritFrom widget也可能會發(fā)生變化点楼,所以正確的做法應(yīng)該在在build()方法或didChangeDependencies()中調(diào)用它扫尖。

  • didChangeDependencies():當(dāng)State對象的依賴發(fā)生變化時會被調(diào)用;例如:在之前build() 中包含了一個InheritedWidget 掠廓,然后在之后的build() 中Inherited widget發(fā)生了變化换怖,那么此時InheritedWidget的子 widget 的didChangeDependencies()回調(diào)都會被調(diào)用。典型的場景是當(dāng)系統(tǒng)語言 Locale 或應(yīng)用主題改變時蟀瞧,F(xiàn)lutter 框架會通知 widget 調(diào)用此回調(diào)沉颂。需要注意,組件第一次被創(chuàng)建后掛載的時候(包括重創(chuàng)建)對應(yīng)的didChangeDependencies也會被調(diào)用悦污。

  • build():它主要是用于構(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 框架會調(diào)用widget.canUpdate來檢測 widget 樹中同一位置的新舊節(jié)點,然后決定是否需要更新瘾婿,如果widget.canUpdate返回true則會調(diào)用此回調(diào)蜻牢。正如之前所述,widget.canUpdate會在新舊 widget 的 key 和 runtimeType 同時相等時會返回true偏陪,也就是說在在新舊 widget 的key和runtimeType同時相等時didUpdateWidget()就會被調(diào)用抢呆。例如 當(dāng)父Widget觸發(fā)重建(rebuild)時,系統(tǒng)會調(diào)用子Widget的didUpdateWidget方法

  • deactivate():當(dāng) State 對象從樹中被移除時笛谦,會調(diào)用此回調(diào)抱虐。在一些場景下,F(xiàn)lutter 框架會將 State 對象重新插到樹中饥脑,如包含此 State 對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey 來實現(xiàn))恳邀。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法。

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

StatefulWidget 生命周期如下圖所示:

image.png

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

7. 在 widget 樹中獲取State對象

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

7.1 通過Context獲取

context對象有一個findAncestorStateOfType()方法贤重,該方法可以從當(dāng)前節(jié)點沿著 widget 樹向上查找指定類型的 StatefulWidget 對應(yīng)的 State 對象茬祷。
下面是實現(xiàn)openDrawer的示例:


class MSGetStateObjectRoute extends StatelessWidget {
  const MSGetStateObjectRoute({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("MSGetStateObjectRoute")),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
                  ScaffoldState _state =
                      context.findAncestorStateOfType<ScaffoldState>()!;
                      // 打開抽屜菜單
                  _state.openDrawer();
                },
                child: Text("打開抽屜菜單1"),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

一般來說清焕,如果 StatefulWidget 的狀態(tài)是私有的(不應(yīng)該向外部暴露)并蝗,那么我們代碼中就不應(yīng)該去直接獲取其 State 對象;如果StatefulWidget的狀態(tài)是希望暴露出的(通常還有一些組件的操作方法)秸妥,我們則可以去直接獲取其State對象滚停。但是通過 context.findAncestorStateOfType 獲取 StatefulWidget 的狀態(tài)的方法是通用的,我們并不能在語法層面指定 StatefulWidget 的狀態(tài)是否私有粥惧,所以在 Flutter 開發(fā)中便有了一個默認(rèn)的約定:如果 StatefulWidget 的狀態(tài)是希望暴露出的键畴,應(yīng)當(dāng)在 StatefulWidget 中提供一個of 靜態(tài)方法來獲取其 State 對象,開發(fā)者便可直接通過該方法來獲韧谎起惕;如果 State不希望暴露,則不提供of方法咏删。這個約定在 Flutter SDK 里隨處可見惹想。

上面示例中的Scaffold也提供了一個of方法,我們其實可以通過Scaffold.of(context) 獲取ScaffoldState的

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通過of靜態(tài)方法來獲取ScaffoldState
      ScaffoldState _state = Scaffold.of(context);
      // 打開抽屜菜單
      _state.openDrawer();
    },
    child: Text("打開抽屜菜單2"),
  );
}),

又比如我們想顯示 SnackBar 的話可以通過下面代碼調(diào)用:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text("我是我是SnackBar")));
    },
    child: Text("顯示SnackBar"),
  );
}),

又比如我們想顯示MaterialBanner 的話可以通過下面代碼調(diào)用:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      ScaffoldMessenger.of(context)
          .showMaterialBanner(MaterialBanner(
        content: Text("我是MaterialBanner"),
        actions: [
          TextButton(
            onPressed: () => ScaffoldMessenger.of(context).clearMaterialBanners(),
            child: Text("OK"),
          ),
        ],
      ));
    },
    child: Text("顯示MaterialBanner"),
  );
}),

7.2 通過GlobalKey

Flutter還有一種通用的獲取State對象的方法——通過GlobalKey來獲榷胶嘀粱! 步驟分兩步:

  1. 給目標(biāo)StatefulWidget添加GlobalKey。
//定義一個globalKey, 由于GlobalKey要保持全局唯一性辰狡,我們使用靜態(tài)變量存儲
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //設(shè)置key
    ...  
)
  1. 通過GlobalKey來獲取State對象
_globalKey.currentState.openDrawer()

GlobalKey 是 Flutter 提供的一種在整個 App 中引用 element 的機(jī)制锋叨。如果一個 widget 設(shè)置了GlobalKey,那么我們便可以通過globalKey.currentWidget獲得該 widget 對象宛篇、globalKey.currentElement來獲得 widget 對應(yīng)的element對象娃磺,如果當(dāng)前 widget 是StatefulWidget,則可以通過globalKey.currentState來獲得該 widget 對應(yīng)的state對象叫倍。

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

7.3 獲取State Demo


class MSGetStateObjectRoute extends StatelessWidget {
  MSGetStateObjectRoute({Key? key}) : super(key: key);
  GlobalKey<ScaffoldState> _globalKey = GlobalKey<ScaffoldState>();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _globalKey,
      appBar: AppBar(title: Text("MSGetStateObjectRoute")),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
                  ScaffoldState _state =
                      context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打開抽屜菜單
                  _state.openDrawer();
                },
                child: Text("打開抽屜菜單1"),
              );
            }),
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 直接通過of靜態(tài)方法來獲取ScaffoldState
                  ScaffoldState _state = Scaffold.of(context);
                  // 打開抽屜菜單
                  _state.openDrawer();
                },
                child: Text("打開抽屜菜單2"),
              );
            }),
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 直接通過globalKey獲取ScaffoldState
                  ScaffoldState _state = _globalKey.currentState!;
                  // 打開抽屜菜單
                  _state.openDrawer();
                },
                child: Text("打開抽屜菜單3"),
              );
            }),
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  ScaffoldMessenger.of(context)
                      .showSnackBar(SnackBar(content: Text("我是我是SnackBar")));
                },
                child: Text("顯示SnackBar"),
              );
            }),
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  ScaffoldMessenger.of(context)
                      .showMaterialBanner(MaterialBanner(
                    content: Text("我是MaterialBanner"),
                    actions: [
                      TextButton(
                        onPressed: () => ScaffoldMessenger.of(context)
                            .clearMaterialBanners(),
                        child: Text("OK"),
                      ),
                    ],
                  ));
                },
                child: Text("顯示MaterialBanner"),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

131.gif

8. 通過 RenderObject 自定義 Widget

StatelessWidget 和 StatefulWidget 都是用于組合其它組件的蛇更,它們本身沒有對應(yīng)的 RenderObject。Flutter 組件庫中的很多基礎(chǔ)組件都不是通過StatelessWidget 和 StatefulWidget 來實現(xiàn)的,比如 Text 派任、Column砸逊、Align等,就好比搭積木掌逛,StatelessWidget 和 StatefulWidget 可以將積木搭成不同的樣子师逸,但前提是得有積木,而這些積木都是通過自定義 RenderObject 來實現(xiàn)的豆混。實際上Flutter 最原始的定義組件的方式就是通過定義RenderObject 來實現(xiàn)篓像,而StatelessWidget 和 StatefulWidget 只是提供的兩個幫助類。

下面我們簡單演示一下通過RenderObject定義組件的方式:

class CustomWidget extends LeafRenderObjectWidget{
  @override
  RenderObject createRenderObject(BuildContext context) {
    // 創(chuàng)建 RenderObject
    return RenderCustomObject();
  }
  @override
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 實現(xiàn)布局邏輯
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 實現(xiàn)繪制
  }
}

如果組件不會包含子組件皿伺,則我們可以直接繼承自 LeafRenderObjectWidget 员辩,它是 RenderObjectWidget 的子類,而 RenderObjectWidget 繼承自 Widget 鸵鸥,我們可以看一下它的實現(xiàn):

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  const LeafRenderObjectWidget({ Key? key }) : super(key: key);

  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

很簡單奠滑,就是幫 widget 實現(xiàn)了createElement 方法,它會為組件創(chuàng)建一個 類型為 LeafRenderObjectElement 的 Element對象妒穴。如果自定義的 widget 可以包含子組件宋税,則可以根據(jù)子組件的數(shù)量來選擇繼承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它們也實現(xiàn)了createElement() 方法讼油,返回不同類型的 Element 對象杰赛。

然后我們重寫了 createRenderObject 方法,它是 RenderObjectWidget 中定義方法汁讼,該方法被組件對應(yīng)的 Element 調(diào)用(構(gòu)建渲染樹時)用于生成渲染對象淆攻。我們的主要任務(wù)就是來實現(xiàn) createRenderObject 返回的渲染對象類,本例中是 RenderCustomObject 嘿架。updateRenderObject 方法是用于在組件樹狀態(tài)發(fā)生變化但不需要重新創(chuàng)建 RenderObject 時用于更新組件渲染對象的回調(diào)瓶珊。

RenderCustomObject 類是繼承自 RenderBox,而 RenderBox 繼承自 RenderObject耸彪,我們需要在 RenderCustomObject 中實現(xiàn)布局伞芹、繪制、事件響應(yīng)等邏輯蝉娜。

Flutter 之 StatelessWidget & StatefulWidget(二)
Flutter 之 生命周期(三)
https://book.flutterchina.club/chapter2/flutter_widget_intro.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唱较,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子召川,更是在濱河造成了極大的恐慌南缓,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荧呐,死亡現(xiàn)場離奇詭異汉形,居然都是意外死亡纸镊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門概疆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逗威,“玉大人,你說我怎么就攤上這事岔冀】瘢” “怎么了株茶?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵矮烹,是天一觀的道長。 經(jīng)常有香客問我乃坤,道長童漩,這世上最難降的妖魔是什么弄贿? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮矫膨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘期奔。我一直安慰自己侧馅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布呐萌。 她就那樣靜靜地躺著馁痴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肺孤。 梳的紋絲不亂的頭發(fā)上罗晕,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機(jī)與錄音赠堵,去河邊找鬼小渊。 笑死,一個胖子當(dāng)著我的面吹牛茫叭,可吹牛的內(nèi)容都是我干的酬屉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼揍愁,長吁一口氣:“原來是場噩夢啊……” “哼呐萨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起莽囤,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤谬擦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后朽缎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惨远,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蔚舀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锨络。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赌躺。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖羡儿,靈堂內(nèi)的尸體忽然破棺而出礼患,到底是詐尸還是另有隱情,我是刑警寧澤掠归,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布缅叠,位于F島的核電站,受9級特大地震影響虏冻,放射性物質(zhì)發(fā)生泄漏肤粱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一厨相、第九天 我趴在偏房一處隱蔽的房頂上張望领曼。 院中可真熱鬧,春花似錦蛮穿、人聲如沸庶骄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单刁。三九已至,卻和暖如春府适,著一層夾襖步出監(jiān)牢的瞬間羔飞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工檐春, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留逻淌,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓喇聊,卻偏偏與公主長得像恍风,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子誓篱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容