在〈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
,被重構到了HelloWidget
的 build
,可以自訂的部份有文字、顏色、寬度等資訊,這可以在建構 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 開發時,會使用 StatelessWidget
、StatefulWidget
來組合、建構使用者介面,實際該怎麼描繪畫面,是 Flutter 的事,只需要知道 RenderObjectWidget
的子類實例會處理這件事就可以了。
也就是說,在組建畫面時,最常關心的是 build
怎麼寫,對於 StatelessWidget
,它的狀態不會改變,想改變 StatelessWidget
的屬性,就是指定新的屬性來重新建立 Widget
。
除了知道必須傳回 Widget
之外,來關心一下 build
的參數 BuildContext
,從型態名稱上來看,可以知道它與建構 Widget
時的情境資訊有關,具體來說,就是包含了 Widget
樹的資訊,例如,可透過 BuildContext
的 findAncestorWidgetOfExactType
,找出 Widget
樹中指定型態的父裔 Widget
。
其實 build
傳入的是實作了 BuildContext
的 Element
實例,每當 Widget
被置放到 Widget
樹時,就會生成一個 Element
實例,而且一個 Widget
的組態,可能會生成多個 Element
實例,Element
實例用來組成 Element
樹,Element
類別的定義,主要作用就是用來維護 Element
樹,這棵樹與 Widget
樹是對應的,兩者的關係之後再來談。
通常你不會直接操作 BuildContext
的方法,而是傳給另一個 API,例如傳給 Navigator
、Theme
的相關方法作為引數,若直接操作 BuildContext
的方法,可能意謂著你在建構 Widget
的過程中,與 Widget
樹的結構產生相依性,這會影響自訂 Widget
的獨立性,通常不是個好的設計。
然而有時,某些資料是可以在子樹的 Widget
間共用的,例如佈景主題相關屬性,這時就可透過 BuildContext
的方法來簡化共用的方式,通常這會透過 InheritedWidget
搭配 BuildContext
來達到,之後再來談。