Widget 概覽


在 Flutter 中,只要是與使用者介面相關的資訊,幾乎都跟 Widget 有關,開發者主要就是使用 Widget 描述畫面,文字、圖片、Icon 等使用者介面元件,都使用 Widget 來描述,看不到的東西也是 Widget,例如佈局相關的描述,也是使用 Widget

如〈Hello, World!〉中談到的,Flutter 應用程式的進入點,就是接受 WidgetrunApp,而當中接收的 Text,就是個 Widget 的子類後裔,嚴格來說,是 StatelessWidget 的子類:

Widget > StatelessWidget > Text

StatelessWidget 主要用於描述狀態不變的使用者介面元件,狀態不變的意思是,不會有方法定義可以改變 StatelessWidget 實例的狀態,你繼承 StatelessWidget 定義自己的 Widget 時,也不該有方法可以改變狀態。

這時你就會有疑問了,在某些操作後文字內容不是會改變嗎?這是因為建立了新的 Text 實例來描述文字,然後畫面重新組建,因此看到了新的文字內容,這通常跟 Widget 的子類 StatefulWidget 有關,之後會看到。

Widget 在設計時,往往考慮的是元件的獨立、可組態、可重用,儘量不與其他元件相依,也就是說,只管自己的職責,例如 Text,想想看你在操作文書處理軟體時,對於文字,考慮的是文字本身、文字對齊、顏色、粗體等資訊,這些可以在 Text 上設定。

例如,將文字改為黃色,可以透過 style

import 'package:flutter/material.dart';

void main() => runApp(
  Text(
    'Hello, World!',
    textAlign: TextAlign.center,
    textDirection: TextDirection.ltr,
    style: TextStyle(color: Colors.yellow),
  )
);

最後呈現出來的畫面如下:

Widget 概覽

目前的文字怎麼都一直顯示在螢幕頂端?Text 可以設置它顯示在螢幕中間嗎?這不是 Text 本身關心的事,因此 Text 不包含這些描述。

有關於元件的佈局,例如置中對齊、行排列、列排列等,是由 Widget 另一子類 RenderObjectWidget 來描述,例如方才談到的顯示在螢幕中間,可以使用 Center

import 'package:flutter/material.dart';

void main() => runApp(
  Center(
    child: Text(
      'Hello, World!',
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
      style: TextStyle(color: Colors.yellow),
    ),
  )
);

顯示結果如下:

Widget 概覽

Center 實際上就是 RenderObjectWidget 的子裔:

Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Align > Center

Center 的父類是 Align,看名稱就知道跟對齊有關,實際上,Center 只是預設為置中對齊的 Align,因為 Center 的原始碼是:

class Center extends Align {
  const Center({ Key key, double widthFactor, double heightFactor, Widget child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

簡單來說,只是呼叫父類建構式罷了,其中並沒有指定 alignment 命名參數,而 Align 的預設值是 Alignment.center,也就是置中:

class Align extends SingleChildRenderObjectWidget {
  const Align({
    Key key,
    this.alignment = Alignment.center, // 預設就是置中
    this.widthFactor,
    this.heightFactor,
    Widget child,
  }) : assert(alignment != null),
       assert(widthFactor == null || widthFactor >= 0.0),
       assert(heightFactor == null || heightFactor >= 0.0),
       super(key: key, child: child);

    ...
}

SingleChildRenderObjectWidget 一看也就知道,它只會包含一個 Widget 元件,使用 child 命名參數來指定;除了 CenterAlign 之外,之後你可能還會很常使用的還有 RowColumn,它們也是 RenderObjectWidget 的子裔,以 Column 為例:

Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Column

MultiChildRenderObjectWidget 表示,Row 可以包含多個 Widget 元件,使用 使用 children 命名參數來指定,Flex 表示它會使用一維陣列的方式來顯示元件。

例如,若整個畫面作為一行(Column),在其中顯示兩個文字:

import 'package:flutter/material.dart';

void main() => runApp(
  Column(
    children: <Widget> [
      Text(
        'Hello, World!',
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.yellow),
      ),
      Text(
        '哈囉!世界!',
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.blue),
      ),
    ],
    mainAxisAlignment: MainAxisAlignment.center
  )
);

範例程式中看到的 <Widget> [....] 會建立 Dart 的 List<Widget> 是 Dart 的泛型語法,表示 List 中都是 Widget,Android 會自動產生這個泛型語法,不少文件也會寫出來,不過其實只要編譯器能推斷型態,不寫 <Widget> 也是可以的,例如:

import 'package:flutter/material.dart';

void main() => runApp(
  Column(
    children: [
      Text(
        'Hello, World!',
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.yellow),
      ),
      Text(
        '哈囉!世界!',
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
        style: TextStyle(color: Colors.blue),
      ),
    ],
    mainAxisAlignment: MainAxisAlignment.center
  )
);

mainAxisAlignment 設定為 MainAxisAlignment.center 表示置中對齊元件,結果就會如下:

Widget 概覽

那麼可不可以改一下背景顏色?有關於元件的繪製、邊距(margin)、墊充(padding)、大小等,是由 Container 負責,它是 StatelessWidget 的子類:

Widget > StatelessWidget > Container

來隨意地設定一下元件的顏色、邊距與寬度:

import 'package:flutter/material.dart';

void main() => runApp(
  Column(
    children: [
      Container(
        child: Text(
          'Hello, World!',
          textAlign: TextAlign.center,
          textDirection: TextDirection.ltr,
          style: TextStyle(color: Colors.yellow),
        ),
        color: Colors.blue,
        margin: const EdgeInsets.all(20.0),
        width: 200.0
      ),
      Container(
        child: Text(
          '哈囉!世界!',
          textAlign: TextAlign.center,
          textDirection: TextDirection.ltr,
          style: TextStyle(color: Colors.blue),
        ),
        color: Colors.yellow,
        margin: const EdgeInsets.all(20.0),
        width: 400.0
      ),
    ],
    mainAxisAlignment: MainAxisAlignment.center
  )
);

結果畫面如下:

Widget 概覽

以上範例中都有些設定細節,目前來說並不用太關心它們,現階段最重要的是,認識 Widget 等相關子類的繼承關係,以及各自負責的職責大致為何,這樣面對日後一堆畫面範例設定,才不會搞不清楚,哪個可以設定給哪個,而目前的 Widget 樹是長這樣:

Widget 概覽

雖然 Widget 樹還很簡單,然而程式碼開始傷眼了,你有辦法看一眼就大致能想像出畫面概貌嗎?當元件描述日趨複雜後,就該考慮重構,將元件抽取出來獨立設計了。