自訂 StatelessWidget


在〈Widget 概覽〉中,最後的範例程式碼開始不易閱讀了,Column 中的兩個子 Widget,其實有著相同的結構,只是組態設定不同,是時候將它們重構,抽取為可重用 Widget 的時候了。

若想基於低階的 Widget 來建立自訂 Widget,若該 Widget 不需要擁有可變狀態,可以繼承 StatelessWidget,它的原始碼其實是長這樣:

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

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

  @protected
  Widget build(BuildContext context);
}

在繼承 StatelessWidget 後,最主要的是實作 build 方法,傳回自訂的 Widget,例如:

import 'package:flutter/material.dart';

void main() => runApp(
  Column(
    children: [
      HelloWidget(
        text: 'Hello, World!',
        textColor: Colors.yellow,
        backgroundColor: Colors.blue,
        width: 200.0,
      ),
      HelloWidget(
        text: '哈囉!世界!',
        textColor: Colors.blue,
        backgroundColor: Colors.yellow,
        width: 400.0,
      ),
    ],
    mainAxisAlignment: MainAxisAlignment.center
  )
);

class HelloWidget extends StatelessWidget {
  final String text;
  final Color textColor;
  final Color backgroundColor;
  final double width;

  HelloWidget({this.text, this.textColor, this.backgroundColor, this.width});

  @override
  Widget build(BuildContext context) {
    return Container(
        child: Text(
          this.text,
          textAlign: TextAlign.center,
          textDirection: TextDirection.ltr,
          style: TextStyle(color: this.textColor),
        ),
        color: this.backgroundColor,
        margin: const EdgeInsets.all(20.0),
        width: this.width,
    );
  }
}

原本 Column 中結構相同的兩個子 Widget,被重構到了HelloWidgetbuild,可以自訂的部份有文字、顏色、寬度等資訊,這可以在建構 HelloWidget 時指定給建構式,現在看一下 runApp 的部份,可以清楚地看出 Column 中有兩個 HelloWidget,如果你沒興趣看 HelloWidget 怎麼構成,就是信任提供 HelloWidget 的開發者寫的程式碼。

runApp 接收的根 Widget 開始,Flutter 會依序呼叫 build 取得 Widget,直到整個使用者介面需要的資訊完備為止;在〈Hello, World!〉中,只使用到 Text,實際上 Text 的原始碼長這樣:

class Text extends StatelessWidget {
    ...

  @override
  Widget build(BuildContext context) {
    ...

    Widget result = RichText(
      textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
      ...
    );

    ...
    return result;
  }    
}

也就是說,Text 也是繼承 StatelessWidget,傳回了 Widget 實現,也就是 RichText 實例,RichText 其實是 RenderObjectWidget 子裔,在〈Widget 概覽〉中談過,有關於元件的佈局,例如置中對齊、行排列、列排列等,是由 Widget 子類 RenderObjectWidget 來描述,也就是說,實際上該怎麼描繪畫面,是由 RenderObjectWidget 來提供資訊。

方才談到,從 runApp 接收的根 Widget 開始,Flutter 會依序呼叫 build 取得 Widget,直到整個使用者介面需要的資訊完備為止,更具體來說,是能從直到底層的 RenderObjectWidget,拿到 RenderObject 建構出完整的渲染樹(render tree)為止。

絕大多數的情況,基於 Flutter 開發時,會使用 StatelessWidgetStatefulWidget 來組合、建構使用者介面,實際該怎麼描繪畫面,是 Flutter 的事,只需要知道 RenderObjectWidget 的子類實例會處理這件事就可以了。

也就是說,在組建畫面時,最常關心的是 build 怎麼寫,對於 StatelessWidget,它的狀態不會改變,想改變 StatelessWidget 的屬性,就是指定新的屬性來重新建立 Widget

除了知道必須傳回 Widget 之外,來關心一下 build 的參數 BuildContext,從型態名稱上來看,可以知道它與建構 Widget 時的情境資訊有關,具體來說,就是包含了 Widget 樹的資訊,例如,可透過 BuildContextfindAncestorWidgetOfExactType,找出 Widget 樹中指定型態的父裔 Widget

其實 build 傳入的是實作了 BuildContextElement 實例,每當 Widget 被置放到 Widget 樹時,就會生成一個 Element 實例,而且一個 Widget 的組態,可能會生成多個 Element 實例,Element 實例用來組成 Element 樹,Element 類別的定義,主要作用就是用來維護 Element 樹,這棵樹與 Widget 樹是對應的,兩者的關係之後再來談。

通常你不會直接操作 BuildContext 的方法,而是傳給另一個 API,例如傳給 NavigatorTheme 的相關方法作為引數,若直接操作 BuildContext 的方法,可能意謂著你在建構 Widget 的過程中,與 Widget 樹的結構產生相依性,這會影響自訂 Widget 的獨立性,通常不是個好的設計。

然而有時,某些資料是可以在子樹的 Widget 間共用的,例如佈景主題相關屬性,這時就可透過 BuildContext 的方法來簡化共用的方式,通常這會透過 InheritedWidget 搭配 BuildContext 來達到,之後再來談。