第二章: 第一個Flutter應(yīng)用 2.2 Widget簡介

跟隨《Flutter實(shí)戰(zhàn)·第二版》學(xué)習(xí)运挫,建議直接看原書

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)行實(shí)踐處理的审葬,所以記住深滚,F(xiàn)lutter 中萬物皆為Widget

Widget接口

在 Flutter 中奕谭, widget 的功能是“描述一個UI元素的配置信息”,它就是說痴荐, Widget 其實(shí)并不是表示最終繪制在設(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 中定義的屬性變化呢?這是因?yàn)楹媳危現(xiàn)lutter 中如果屬性發(fā)生則會重新構(gòu)建Widget樹击敌,即重新創(chuàng)建新的 Widget 實(shí)例來替換舊的 Widget 實(shí)例,所以允許 Widget 的屬性變化是沒有意義的拴事,因?yàn)橐坏?Widget 自己的屬性變了自己就會被替換沃斤。這也是為什么 Widget 中定義的屬性必須是 final 的原因。
  • widget類繼承自DiagnosticableTree刃宵, DiagnosticableTree即“診斷樹”衡瓶,主要作用是提供調(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é)點(diǎn)的Element對象坦袍。此方法是 Flutter 框架隱式調(diào)用的诚撵,在我們開發(fā)過程中基本不會調(diào)用到。
  • debugFillProperties(...) 復(fù)寫父類的方法键闺,主要是設(shè)置診斷樹的一些特性
  • canUpdate(...)是一個靜態(tài)方法,它主要用于在 widget 樹重新build時復(fù)用舊的 widget 澈驼,其實(shí)具體來說辛燥,應(yīng)該是:是否用新的 widget 對象去更新舊UI樹上所對應(yīng)的Element對象的配置;通過其源碼我們可以看到缝其,只要new widget與old widget的runtimeType和key同時相等時就會用new widget去更新Element對象的配置挎塌,否則就會創(chuàng)建新的Element。

現(xiàn)在只需知道内边,為 widget 顯式添加 key 的話可能(但不一定)會使UI在重新構(gòu)建時變的高效榴都,讀者目前可以先忽略此參數(shù)

另外Widget類本身是一個抽象類,其中最核心的就是定義了createElement()接口漠其,在 Flutter 開發(fā)中嘴高,我們一般都不用直接繼承Widget類來實(shí)現(xiàn)一個新組件竿音,相反,我們通常會通過繼承StatelessWidget或StatefulWidget來間接繼承widget類來實(shí)現(xiàn)拴驮。StatelessWidget和StatefulWidget都是直接繼承自Widget類春瞬,而這兩個類也正是 Flutter 中非常重要的兩個抽象類,它們引入了兩種 widget 模型

Flutter中的四棵樹

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

  1. 根據(jù) Widget 樹生成一個 Element 樹潜沦,Element 樹中的節(jié)點(diǎn)都繼承自 Element 類萄涯。
  2. 根據(jù) Element 樹生成 Render 樹(渲染樹),渲染樹中的節(jié)點(diǎn)都繼承自RenderObject 類唆鸡。
  3. 根據(jù)渲染樹生成 Layer 樹涝影,然后上屏顯示,Layer 樹中的節(jié)點(diǎn)都繼承自 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)淀衣。比如 StatelessWidget 和 StatefulWidget 都沒有對應(yīng)的 RenderObject。
  2. 渲染樹在上屏前會生成一棵 Layer 樹
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),
      ),
    );
  }
}

上面的代碼艺沼,實(shí)現(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

運(yùn)行后效果如下:


Simulator Screen Shot - iPhone 13.png
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)操作“的一個句柄(handle)篮迎,比如它提供了從當(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.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此處實(shí)際上是Text("Context測試")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}
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)用該方法為每一個位置生成一個獨(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 框架狀態(tài)發(fā)生改變旁理,F(xiàn)lutter 框架在收到消息后,會重新調(diào)用其build方法重新構(gòu)建 widget 樹我磁,從而達(dá)到更新UI的目的孽文。

State 中有兩個常用屬性:

  1. widget驻襟,它表示與該 State 實(shí)例關(guān)聯(lián)的 widget 實(shí)例,由Flutter 框架動態(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 框架會動態(tài)設(shè)置State. widget 為新的 widget 實(shí)例。
  2. context稚失。StatefulWidget對應(yīng)的 BuildContext栋艳,作用同StatelessWidget 的BuildContext。
State生命周期

理解State的生命周期對flutter開發(fā)非常重要

仍然以計(jì)數(shù)器功能為例句各,實(shí)現(xiàn)一個計(jì)數(shù)器 CounterWidget 組件 吸占,點(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() => _CounterWidgetState();
}

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

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    print('build');

    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text('$_counter'),
          //點(diǎn)擊后計(jì)數(shù)器自增
          onPressed: () => setState(() => ++_counter),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(covariant CounterWidget oldWidget) {
    // TODO: implement didUpdateWidget
    super.didUpdateWidget(oldWidget);
    print('didUpdateWidget');
  }

  @override
  void deactivate() {
    // TODO: implement deactivate
    super.deactivate();
    print('deactivate');
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    print('dispose');
  }

  @override
  void reassemble() {
    // TODO: implement reassemble
    super.reassemble();
    print('reassemble');
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    print('didChangeDependencies');
  }
}

接下來问拘,我們創(chuàng)建一個新路由,在新路由中惧所,我們只顯示一個CounterWidget:

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

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return CounterWidget();
  }
}

在計(jì)數(shù)器自增方法下骤坐,新增一個打開路由方法:


image.png
image.png

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

flutter: initState
flutter: didChangeDependencies
flutter: build
Simulator Screen Shot - iPhone 13.png

可以看到,在StatefulWidget插入到 widget 樹時首先initState方法會被調(diào)用势似。
然后我們點(diǎn)擊??按鈕熱重載拌夏,控制臺輸出日志如下:

flutter: reassemble
flutter: didUpdateWidget
flutter: build

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

接下來障簿,我們在 widget 樹中移除CounterWidget,將 StateLifecycleTest 的 build方法改為:

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

然后熱重載栅迄,日志如下:

flutter: reassemble
flutter: deactivate
flutter: dispose

我們可以看到站故,在CounterWidget從 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橱乱,關(guān)于InheritedWidget我們將在后面章節(jié)介紹),原因是在初始化完成后赁豆, widget 樹中的InheritFrom widget也可能會發(fā)生變化仅醇,所以正確的做法應(yīng)該在在build()方法或didChangeDependencies()中調(diào)用它。
  • didChangeDependencies():當(dāng)State對象的依賴發(fā)生變化時會被調(diào)用魔种;例如:在之前build() 中包含了一個InheritedWidget析二,然后在之后的build() 中Inherited widget發(fā)生了變化,那么此時Inherited widget的子 widget 的didChangeDependencies()回調(diào)都會被調(diào)用节预。典型的場景是當(dāng)系統(tǒng)語言 Locale 或應(yīng)用主題改變時叶摄,F(xiàn)lutter 框架會通知 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 框架會調(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 框架會將 State 對象重新插到樹中翻翩,如包含此 State 對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey 來實(shí)現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法。
  • dispose():當(dāng) State 對象從樹中被永久移除時調(diào)用体斩;通常在此回調(diào)中釋放資源。

StatefulWidget 生命周期如圖

image.png

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

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

為什么build()方法放在State(而不是StatefulWidget)中 蹬敲?這主要是為了提高開發(fā)的靈活性。如果將build()方法在StatefulWidget中則會有兩個問題:

  • 狀態(tài)訪問不便莺戒。
    試想一下伴嗡,如果我們的StatefulWidget有很多狀態(tài),而每次狀態(tài)改變都要調(diào)用build方法从铲,由于狀態(tài)是保存在State中的瘪校,如果build方法在StatefulWidget中,那么build方法和狀態(tài)分別在兩個類中名段,那么構(gòu)建時讀取狀態(tài)將會很不方便阱扬!試想一下,如果真的將build方法放在StatefulWidget中的話伸辟,由于構(gòu)建用戶界面過程需要依賴State麻惶,所以build方法將必須加一個State參數(shù),大概是下面這樣:
widget build(BuildContext context, State state){
      //state.counter
      ...
  }

這樣的話就只能將State的所有狀態(tài)聲明為公開的狀態(tài)信夫,這樣才能在State類外部訪問狀態(tài)窃蹋!但是,將狀態(tài)設(shè)置為公開后静稻,狀態(tài)將不再具有私密性警没,這就會導(dǎo)致對狀態(tài)的修改將會變的不可控。但如果將build()方法放在State中的話姊扔,構(gòu)建過程不僅可以直接訪問狀態(tài)惠奸,而且也無需公開私有狀態(tài),這會非常方便恰梢。

  • 繼承StatefulWidget不便
    例如佛南,F(xiàn)lutter中有一個動畫 widget 的基類AnimatedWidget,它繼承自StatefulWidget類嵌言。AnimatedWidget中引入了一個抽象方法build(BuildContext context)嗅回,繼承自AnimatedWidget的動畫 widget 都要實(shí)現(xiàn)這個build方法。現(xiàn)在設(shè)想一下摧茴,如果StatefulWidget 類中已經(jīng)有了一個build方法绵载,正如上面所述,此時build方法需要接收一個state對象,這就意味著AnimatedWidget必須將自己的State對象(記為_animatedWidgetState)提供給其子類娃豹,因?yàn)樽宇愋枰谄鋌uild方法中調(diào)用父類的build方法焚虱,代碼可能如下:
class MyAnimationWidget  extends AnimatedWidget {
    @override
    Widget build(BuildContext context, State state){
      //由于子類要用到AnimatedWidget 的狀態(tài)對象_animatedWidgetState,
      //所以AnimatedWidget 必須通過某種方式將其狀態(tài)對象_animatedWidgetState
      //暴露給其子類   
      super.build(context, _animatedWidgetState)
    }
}

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

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

綜上所述,可以發(fā)現(xiàn)蓬抄,對于StatefulWidget丰嘉,將build方法放在State中,可以給開發(fā)帶來很大的靈活性嚷缭。

在widget樹種獲取state對象

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

通過Context獲取

context對象有一個findAncestorStateOfType()方法优床,該方法可以從當(dāng)前節(jié)點(diǎn)沿著 widget 樹向上查找指定類型的 StatefulWidget 對應(yīng)的 State 對象

下面是實(shí)現(xiàn)打開 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子樹中獲取State對象"),
      ),
      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方法瀑粥,我們其實(shí)是可以直接調(diào)用它的:

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

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

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

完整示例:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
//      home: const MyHomePage(title: 'Flutter Demo Home Page'),
//    home: const MyHomePage(myTitle: 'Hi'),
    home: const GetStateObjectRoute(),
    );
  }
}

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子樹中獲取State對象"),
      ),
      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: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text("我是SnackBar")),
                  );
                },
                child: Text('顯示SnackBar'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

運(yùn)行效果:


Simulator Screen Shot - iPhone 13.png
通過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ù)。

通過 RenderObject 自定義 Widget

StatelessWidget 和 StatefulWidget 都是用于組合其它組件的比然,它們本身沒有對應(yīng)的 RenderObject丈氓。Flutter 組件庫中的很多基礎(chǔ)組件都不是通過StatelessWidget 和 StatefulWidget 來實(shí)現(xiàn)的,比如 Text 强法、Column万俗、Align等,就好比搭積木饮怯,StatelessWidget 和 StatefulWidget 可以將積木搭成不同的樣子闰歪,但前提是得有積木,而這些積木都是通過自定義 RenderObject 來實(shí)現(xiàn)的蓖墅。實(shí)際上Flutter 最原始的定義組件的方式就是通過定義RenderObject 來實(shí)現(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() {
    // 實(shí)現(xiàn)布局邏輯
  }

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

如果組件不會包含子組件论矾,則我們可以直接繼承自 LeafRenderObjectWidget 教翩,它是 RenderObjectWidget 的子類,而 RenderObjectWidget 繼承自 Widget 贪壳,我們可以看一下它的實(shí)現(xiàn):

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

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

很簡單饱亿,就是幫 widget 實(shí)現(xiàn)了createElement 方法,它會為組件創(chuàng)建一個 類型為 LeafRenderObjectElement 的 Element對象闰靴。如果自定義的 widget 可以包含子組件彪笼,則可以根據(jù)子組件的數(shù)量來選擇繼承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它們也實(shí)現(xiàn)了createElement() 方法蚂且,返回不同類型的 Element 對象

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

RenderCustomObject 類是繼承自 RenderBox凡伊,而 RenderBox 繼承自 RenderObject零渐,我們需要在 RenderCustomObject 中實(shí)現(xiàn)布局、繪制系忙、事件響應(yīng)等邏輯诵盼,關(guān)于如何實(shí)現(xiàn)這些邏輯,涉及到的知識點(diǎn)會貫穿本書银还,現(xiàn)在先不要著急风宁,我們會在后面的章節(jié)中逐步介紹

Flutter SDK內(nèi)置組件庫介紹

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

import 'package:flutter/widgets.dart';

介紹一下常用的組件捺弦。

基礎(chǔ)組件
  • Text: 該組件可讓您創(chuàng)建一個帶格式的文本
  • Row饮寞、Column: 這些具有彈性空間的布局類 widget 可讓您在水平(Row)和垂直(Column)方向上創(chuàng)建靈活的布局。其設(shè)計(jì)是基于 Web 開發(fā)中的 Flexbox 布局模型
  • Stack: 取代線性布局
  • Container: Container 可讓您創(chuàng)建矩形視覺元素列吼。Container 可以裝飾一個BoxDecoration, 如 background幽崩、一個邊框、或者一個陰影寞钥。 Container也可以具有邊距(margins)慌申、填充(padding)和應(yīng)用于其大小的約束(constraints)。另外理郑, Container可以使用矩陣在三維空間中對其進(jìn)行變換太示。
Material組件

Flutter 提供了一套豐富 的Material 組件,它可以幫助我們構(gòu)建遵循 Material Design 設(shè)計(jì)規(guī)范的應(yīng)用程序香浩。Material 應(yīng)用程序以MaterialApp組件開始类缤, 該組件在應(yīng)用程序的根部創(chuàng)建了一些必要的組件,比如Theme組件邻吭,它用于配置應(yīng)用的主題餐弱。
是否使用MaterialApp完全是可選的,但是使用它是一個很好的做法囱晴。在之前的示例中膏蚓,我們已經(jīng)使用過多個 Material 組件了,如:Scaffold畸写、AppBar驮瞧、TextButton等。要使用 Material 組件枯芬,需要先引入它:

import 'package:flutter/material.dart';
Cupertino組件

Flutter 也提供了一套豐富的 Cupertino 風(fēng)格的組件论笔,盡管目前還沒有 Material 組件那么豐富采郎,但是它仍在不斷的完善中。值得一提的是在 Material 組件庫中有一些組件可以根據(jù)實(shí)際運(yùn)行平臺來切換表現(xiàn)風(fēng)格狂魔,比如MaterialPageRoute蒜埋,在路由切換時,如果是 Android 系統(tǒng)最楷,它將會使用 Android 系統(tǒng)默認(rèn)的頁面切換動畫(從底向上)整份;如果是 iOS 系統(tǒng),它會使用 iOS 系統(tǒng)默認(rèn)的頁面切換動畫(從右向左)籽孙。
由于在前面的示例中還沒有Cupertino組件的示例烈评,下面我們實(shí)現(xiàn)一個簡單的 Cupertino 組件風(fēng)格的頁面:

導(dǎo)入cupertino widget庫
import 'package:flutter/cupertino.dart';

class CupertinoTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
          color: CupertinoColors.activeBlue,
          child: Text("Press"),
          onPressed: (){},
        ),
      ),
    );
  }
}
image.png
Simulator Screen Shot - iPhone 13.png
Simulator Screen Shot - iPhone 13.png
總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芽卿,一起剝皮案震驚了整個濱河市揭芍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卸例,老刑警劉巖称杨,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異筷转,居然都是意外死亡姑原,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門呜舒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锭汛,“玉大人,你說我怎么就攤上這事袭蝗』脚梗” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵到腥,是天一觀的道長朵逝。 經(jīng)常有香客問我,道長乡范,這世上最難降的妖魔是什么配名? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任啤咽,我火速辦了婚禮,結(jié)果婚禮上段誊,老公的妹妹穿的比我還像新娘闰蚕。我一直安慰自己,他們只是感情好连舍,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布没陡。 她就那樣靜靜地躺著,像睡著了一般索赏。 火紅的嫁衣襯著肌膚如雪盼玄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天潜腻,我揣著相機(jī)與錄音埃儿,去河邊找鬼。 笑死融涣,一個胖子當(dāng)著我的面吹牛童番,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播威鹿,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼剃斧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了忽你?” 一聲冷哼從身側(cè)響起幼东,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎科雳,沒想到半個月后根蟹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡糟秘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年简逮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尿赚。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡买决,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吼畏,到底是詐尸還是另有隱情督赤,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布泻蚊,位于F島的核電站躲舌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏性雄。R本人自食惡果不足惜没卸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一羹奉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧约计,春花似錦诀拭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尉桩,卻和暖如春筒占,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜘犁。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工翰苫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人这橙。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓奏窑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屈扎。 傳聞我的和親對象是個殘疾皇子埃唯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

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