iT邦幫忙

2023 iThome 鐵人賽

DAY 8
1
Mobile Development

Flutter 從零到實戰 - 30 天の學習筆記系列 第 8

[Day 08] Flutter 基礎 widget:Text、Button 與排版元件

  • 分享至 

  • xImage
  •  

今天我們要來講解 Flutter 的基礎Widget。

Flutter 預設提供了兩種熱門的 UI 組件,分別是接近 Andorid 原生風格的 Material 以及接近 iOS 原生風格的 Cupertino 可供開發者使用,使得在開發者可以透過呼叫這些定義好的 UI 組件,快速的組合出應用程式。

所以讓我們再重新的檢視一下昨天我們最後寫的 Hello World 程式碼

// 引入 flutter 預載好的 material.dart UI 庫
import 'package:flutter/material.dart';

// 應用程式起始點
void main() {
  // 開始執行應用程式,並呼叫 MaterialApp 建構子,表示我的應用程式為 Material style
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: Scaffold(
      appBar: AppBar(
        title: Text('Flutter Playground'),
      ),
      body: Center(
        child: Text('Hello World'),
      ),
    ),
  ));
}

我們使用了 Material 作為我們預設的樣式,接著就是不斷的找尋是否有適合的 widget 可以讓我們進行呼叫,開始組合與微調。

由於篇幅因素,內容並無法涵蓋所有的 widget,以下我們將介紹幾個基礎的 widget 使用方法,除了持續壯大我們的 Flutter playground,更藉此能一步步的熟悉 flutter widget 使用方式,最終你就可以自己試著對照文件將各式各樣的 widget 加入到你的應用程式中。

Text

顧名思義 Text 是用於顯示文字的工具,也可以針對文字進行樣式調整、對齊位置等等。我們需要使用到一個 widget 類別時,首先一定得先使用建構子來初始化我們將要宣告的物件。讓我們來看看 Text 的建構子是如何進行定義的

VS Code 檢視 widget class 的使用方式有兩種:

  1. 將滑鼠的游標移至該 widget 上方,VS Code 便會跳出一個小視窗,裡面會寫定義
  2. 在 widget 上方點選右鍵 並選擇「移至定義」就會導向定義該 widget 的檔案中
    在我的 Flutter 版本中對於 Text 建構子如下,不同版本的可能會有些許出入不過並不影響我們怎麼閱讀此元件。在 Text 類別的定義中,第一個參數無需具名但是一定要夾帶;剩下用 {} 括起來的便是前面章節中學到的 named parameters 皆可選擇性的夾帶。
const Text(
  String this.data, {
  super.key,
  this.style,
  this.strutStyle,
  this.textAlign,
  this.textDirection,
  this.locale,
  this.softWrap,
  this.overflow,
  this.textScaleFactor,
  this.maxLines,
  this.semanticsLabel,
  this.textWidthBasis,
  this.textHeightBehavior,
  this.selectionColor,
}) : ... 後面省略

因此當我們在宣告最基本款的 Text widget 時,我們的宣告方式如下

Text('Hello World');

如此便成功的宣告了一個帶有 Hello World 訊息的 Text widget 。那如果要針對字體進行微調呢?我們來看看上面的 named parameters 中哪個最像是用來定義字體樣式的,應該很好猜就是 style 這個欄位。如果你是從寫前端過來的,當我想將文字樣式改為藍色 且大小為 18px 時,你可能會覺得 style 會是這樣設定:

Text('Hello World', style: 'font-size: 18px; color: blue');

注意!我們在看類別定義時千萬可別漏看了對應的型態!讓我們再一次的前往檢視 style 的型態 (方式與剛剛介紹的相同)。

final TextStyle? style;

首先因為 style 是屬於 optional parameters,因此 TextStyle? 表示賦予的值可為 TextStyle 型態或 null ,表示我們今天要用到 style 時就要找一個型態為 TextStyle 的東西來塞。這時我們再繼續往 TextStyle 的定義往下找:

const TextStyle({
    this.inherit = true,
    this.color,
    this.backgroundColor,
    this.fontSize,
    this.fontWeight,
    this.fontStyle,
    this.letterSpacing,
    this.wordSpacing,
    this.textBaseline,
    this.height,
    this.leadingDistribution,
    this.locale,
    this.foreground,
    this.background,
    this.shadows,
    this.fontFeatures,
    this.fontVariations,
    this.decoration,
    this.decorationColor,
    this.decorationStyle,
    this.decorationThickness,
    this.debugLabel,
    String? fontFamily,
    List<String>? fontFamilyFallback,
    String? package,
    this.overflow,
  }) : ...後面省略

我們來鎖定我們所需要的屬性(字體大小改為 18px;顏色改為藍色)。上方最符合的就是 fontSizecolor ,兩者分別的型態為 doubleColor 。因此我們就填入相應所需要型態的值。最終結果如下:

Text('Hello World', style: TextStyle(fontSize: 18, color: Colors.blue));

花了一點篇幅帶大家從 Text 的定義開始像洋蔥一樣一層一層的往裡面剝開,找到我們所需要的屬性再依據所需要的型態給相應的值。其實其他的 widget 也都大同小異,只要掌握到此方法,便可以融會貫通到其他的地方拉!

ElevatedButton

是由 Material 所定義的按鈕,因此欲引用此 widget 記得要先引入 material package 。我們現在希望可以建立一個 Button ,其中的文字顯示方才的 Hello World 字樣。因此請先將剛剛的文字註解掉,並輸入 ElevatedButton 此時你的 VS Code 應該會跳出自動補全的提示,請按下 Enter ,這時該行應該會變成。

ElevatedButton(onPressed: onPressed, child: child)

我們一樣按照慣例別急著寫,先來檢視 ElevatedButton 的建構子,看看我們需要提供哪些資訊。

const ElevatedButton({
    super.key,
    required super.onPressed,
    super.onLongPress,
    super.onHover,
    super.onFocusChange,
    super.style,
    super.focusNode,
    super.autofocus = false,
    super.clipBehavior = Clip.none,
    super.statesController,
    required super.child,
  });

在類別建構子中全部都是 named parameters ,但因為 onPressedchild 兩個參數有標上 required ,也就表示建構此物件時一定要夾帶此兩個參數。
我們就字面上解析這兩個參數的意義:

  • child:型態為 Widget? 表示這個 button 底下要放 widget,會被包在此按鈕當中
  • onPressed :型態為 VoidCallback? ,再往下翻你會發現 typedef VoidCallback = void Function(),也就是代表 VoidCallback 本身其實是 Function 的另一種寫法。因此 onPressed 是一個函式,用於表示點擊該按鈕後要觸發的事件函式。

typedef :是用來自定義型別的關鍵字,如:typedef IntList = List<int> 便是定義 IntList 這個關鍵字可以用於表示 List<int> 這個型態

因此我們就將剛剛的 Text 放入 ElevatedButtonchild 欄位中。並且希望每次按下按鈕時,都在終端機印出 Hello World 的字樣。

import 'package:flutter/material.dart';

// 應用程式起始點
void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Playground'),
      ),
      body: Center(
          child: ElevatedButton(
              onPressed: () {
                debugPrint('Hello World');
              },
              child: const Text('Hello World',
                  style: TextStyle(fontSize: 18, color: Colors.white)))),
    ),
  ));
}

最後你的結果應該要長這樣,功能可以正常執行,並且編輯器沒有跳出任何的錯誤或是警告。

相信你剛剛在修改的過程當中一定遇到很多次終端機跟你提示「請使用 const 建構子來增進效能」的字樣,原因是因為之前我們有介紹到 const 創建的內容是不變的,表示使用 const 修飾的 widget 在應用程式的生命週期內只會建構一次,使得性能可以有相當大程度的優化。

因此當你的某個部件確定是不會變動的,請使用 const 來進行修飾。不過當你無法判斷時,相信你的編輯器提示會非常「好心」的跳出提示來告訴你,直到你改對為止XD

目前為止,我們介紹了兩種 widget 都僅限於呼叫 widget 並顯示於頁面上。但如果我想要將這些內容進行佈局呢? 這時候就需要一系列的佈局工具 (Layout widget)拉~

Center

其實一開始的程式碼,在 ElevatedButton 之外有使用到 Center 我們還沒有提到。

在 Flutter 中有一個詞為 constraint (約束),指的是在渲染佈局中的限制或是規則,用於確定 widget 的大小和位置。這樣的約束關係是經由 parent 與 child 經由溝通確認後,再由 parent 傳遞給 child 的,告訴子組件應該要如何進行佈局。一旦子組件超越了 constraint 就會跳出錯誤,表示父組件無法容納。

Center 這個 widget 會根據 constraint 將 child 放置於正中間的位置,如我們的範例一樣。

Padding

Padding 是可以用於為子組件添加空白區域,用於調整與周圍的間距。嘗試將方才的 Center widget 拿掉,我們換上 Padding 來看看

Padding(
  padding: const EdgeInsets.all(16.0),
  child: ElevatedButton(
      onPressed: () {
        debugPrint('Hello World');
      },
      child: const Text('Hello World',
          style: TextStyle(fontSize: 18, color: Colors.white))))

我們將 Center 拿掉後,ElevatedButton 會回到預設到左上方 (佈局的預設是由左至右、由上而下),我們設定 child 的 button 四周皆間隔 16 px 的距離。

Row

將多個子組件(children)以水平方向進行排列,不過要注意的是 Row本身並不具備 scroll 的性質,因此在使用前需要先思考是否 children 的寬度足以被 Row 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情

Padding(
  padding: const EdgeInsets.all(16.0),
  child: Row(children: [
    Container(color: Colors.red, width: 100, height: 50),
    Container(color: Colors.indigo, width: 100, height: 60),
    Container(color: Colors.green, width: 100, height: 70),
  ]))

原則上你會看到如下圖的運行結果。
https://ithelp.ithome.com.tw/upload/images/20230923/20135082rWYW2lJcLO.jpg
我們宣告了三個等寬但高度不同的三個 Container 以水平方向進行排列。現在 Row :
- 寬度:螢幕寬度減去左右的16px padding
- 高度:找出最大高度的 child

這時我們來看看 Row 底下的除了 children 外,兩個也很重要用於對齊的參數:

  • mainAxisAlignment :表示水平方向的對齊屬性,預設為 start 表示靠左對齊。其他還有如 end 靠右對齊、center 水平置中對齊等等的屬性
  • crossAxisAlignment :表示垂直方向的對齊屬性,預設為 center 垂直置中。其他還有如 start 垂直靠上、end 垂直靠下等等的屬性

各位可以試著玩玩看,相信你很快就能了解所有屬性!這裡出個小小練習,會在文末進行解答喔~

練習1.

請修改上述程式碼,使得顯示效果可以如下圖。
https://ithelp.ithome.com.tw/upload/images/20230923/20135082S6MgDCOYfw.jpg

Column

與 Row 相對的,Colummn 是將多個子組件(children)以垂直方向進行排列,同樣的是 Column本身並不具備 scroll 的性質,因此在使用前需要先思考是否 children 的長度足以被 Column 所容納,否則會產生錯誤。請替換成以下程式碼,看看會發生什麼事情

Padding(
  padding: const EdgeInsets.all(16.0),
  child: Column(children: [
    Container(color: Colors.red, width: 50, height: 100),
    Container(color: Colors.indigo, width: 60, height: 100),
    Container(color: Colors.green, width: 70, height: 100),  
]))

原則上你會看到如下圖的運行結果。
https://ithelp.ithome.com.tw/upload/images/20230923/20135082vd1TfqfDrS.jpg

我們宣告了三個等高但寬度不同的三個 Container 以垂直方向進行排列。現在 Column :
- 高度:螢幕高度減去上下的16px padding
- 寬度:找出最大寬度的 child
同樣我們也來看看 Column 底下的除了 children 外,兩個也用於對齊的參數:
- mainAxisAlignment :表示垂直方向的對齊屬性,預設為 start 表示靠上對齊。其他還有如 end 靠下對齊、center 垂直置中對齊等等的屬性
- crossAxisAlignment :表示水平方向的對齊屬性,預設為 center 水平置中。其他還有如 start 水平靠左、end 水平靠右等等的屬性

在 Row 的時候 mainAxisAlignment 中文直翻就是在主要軸的對齊屬性,也就是水平軸;crossAxisAlignment 就是轉 90 度交叉的垂直軸的對齊屬性。

換到 Column 的時候就完全相反,mainAxisAlignment 是垂直軸的對齊屬性;crossAxisAlignment 就是相對的水平軸對齊屬性。

這裡我們也一樣出個練習題,作為本篇的結尾。

練習2.

請撰寫程式碼,使得顯示效果可以如下圖。這題會同時混用到 Row 與 Column,可以從 Column 方向開始思考。
https://ithelp.ithome.com.tw/upload/images/20230923/20135082kouAzMhfTB.png

今日總結

今天我們認識:

  1. 從類別定義開始找出 widget 的基礎使用方法
  2. Text 用於基礎顯示文字
  3. ElevatedButton 用於顯示按鈕,並認識運用 onPressed 來觸發事件
  4. Center 將子組件完全的置中
  5. Padding 設定子組件的間隔
  6. Row 將多個子組件以水平方向排列;Column 則是將多個子組件以垂直方向排列

俗話說「給你魚吃,不如教你釣魚」。我們並無法全盤的介紹一輪所有 widget 的使用方式,但藉由今天的介紹相信大家已經具備了查找文件的能力拉(釣魚🐟)!只要找到好的 widget,看一下文件就能夠引用在你的應用程式中了。

另外Flutter 也有提供 MaterialCupertino 各自 component 的樣式任君挑選,趕快來試試找到心儀的 widget 並實作看看吧!

明天我們會來講解 Flutter widget 的重要概念,StatelessStateful widget,準備好了嗎!!明天見囉~

參考解答

  • 練習 1
Row(
  // 水平讓各自 Container 間有間距
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  // 垂直靠下
  crossAxisAlignment: CrossAxisAlignment.end,
  children: [
    Container(color: Colors.red, width: 100, height: 50),
    Container(color: Colors.indigo, width: 100, height: 60),
    Container(color: Colors.green, width: 100, height: 70),
]))
  • 練習2
Column(
  // 先考慮 column 佈局
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    // 第一個 Container 水平靠左
    Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Container(color: Colors.red, width: 50, height: 40),
      ],
    ),
    // 第二個 Container 水平置中
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(color: Colors.indigo, width: 60, height: 40),
      ],
    ),
    // 第三個 Container 水平靠右
    Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: [
        Container(color: Colors.green, width: 70, height: 40),
      ],
    ),
])

上一篇
[Day 07] Flutter 的第一個專案
下一篇
[Day 09] Stateless & Stateful Widget
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言