iT邦幫忙

2024 iThome 鐵人賽

DAY 12
0
Mobile Development

Flutter 開發實戰 - 30 天逃離新手村系列 第 12

Day 12 深入狀態組件的生命週期

  • 分享至 

  • xImage
  •  

在之前的章節,我們看過了狀態組件和無狀態組件的差異以及利用 setState 重新呼叫 build 方法。然而狀態組件還有一些額外的生命週期需要了解,因為它們對於管理輸入資料非常重要。隨著我們探討更進階的組件互動,這些生命週期也會逐漸變得更加重要。

關鍵生命週期狀態

狀態組件會經歷一些關鍵生命週期狀態。我們將會探討一些大部分情況會需要的狀態,後續我們介紹一些在特定情境和案例下所需的生命週期狀態。

狀態的建立

狀態的建立是發生在 StatefulWidget 生命週期最開始的階段,就在建構子呼叫之後。這個組件會通過呼叫 createState() 方法並回傳一個搭配的狀態物件,例如 State<MyWidget> _MyWidgetState ,也就是 State 物件。這是生命週期中必不可少的步驟,,少了這步,狀態組件就無法擁有狀態。

下面是 createState 的範例:

// ⚠️ 注意部分舊版的程式可能會被 analysis_options 的規範警告
class MyHomePage extends StatefulWidget {
  const MyHomePage({ super.key, this.title });
  
  final String title;
  
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // ...
}

範例展示了如何定義一個狀態組件 (StatefulWidget) 和其對應狀態 (State) 的基本框架。接著讓我們來改寫 DestinationWidget 成為狀態組件:

import 'package:flutter/material.dart';

class DestinationWidget extends StatefulWidget {
  const DestinationWidget({super.key, required this.destinationName});

  final String destinationName;

  @override
  State<DestinationWidget> createState() => _DestinationWidgetState();
}

class _DestinationWidgetState extends State<DestinationWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(widget.destinationName);
  }
}

範例中 DestinationWidget 使用了 createState() 並回傳了一個狀態物件,而狀態物件則包含了本來在 StatelessWidget 中的 build。也就是現在介面搬到了狀態物件中來建立。不過這也表示要取得 destinationName 現在我們需要使用引用 widget.destinationNameState 物件可以使用 widget 來取得搭配的 StatefulWidget 物件參考。

為什麼 DestinationWidget 類別內部又可以使用自己 State<DestinationWidget> ?

簡單的說,編譯器在掃描階段就知道了類別的存在,因此可以在 createState() 中使用。類別具體的內容則會在後續的階段解析。且 Dart 具備延遲解析,另外,Dart 允許某些形式的循環引用。例如,類別 A 可以引用類別 B,而類別 B 也可以引用類別 A,這些循環引用在 Dart 中是允許的,前提是編譯器在解析時可以找到所有引用的定義。

狀態初始化 initState

State 的物件實例在 initState() 方法初始化了它的狀態變數和其他基礎條件,例如資料庫連線。這個方法只會在當 Widget 第一次被加入樹狀結構的時候被呼叫一次,然後其呈現在使用者面前,並且這個方法是可選的。

我們後續將會探討一些例子,但 initState() 的基本例子如下:

@override
void initState() {
  super.initState();
}

可以看到這個方法的第一行必須要初始化 super 類別,然後接著你自訂的邏輯初始化狀態。對於我們的 DestinationWidget 我們可以從資料庫讀取資料初始化計數器,範例為了單純我們直接設定 0

class _DestinationWidgetState extends State<DestinationWidget> {
  late int _counter;
  
  @override
  void initState() {
    super.initState();
    // 可以從資料庫讀取資料來初始化
    _counter = 0;
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(widget.destinationName);
  }
}

注意到我們必須使用 late_counter 宣告為延遲初始化變數,這是因為它是在建構子執行之後才進行的。然而它會確保在我們使用它之前就別設置,因此不需要使用 null

補充:基於 Dart Null-Safe 空安全的特性,一但有可能為 null 我們通常要附加宣告例如 int? num。但一旦使用了 null 後續又會增加很多判斷和限制。因此在一些情況,例如我們可以確保在某變數被使用之前一定會賦值,這時我們可以使用 late 延遲初始化。關鍵就是在開發時,要儘量減少可 null 的變數。

  • 一般宣告:必須在宣告時或建構子內完成初始化
  • 可 null 變數:不受限制
  • late :介於一般宣告和可 null 之間,即初始化可以被延遲,保證在該變數被使用之前完成初始化

關鍵字複習:

  • final : 定義變數只能被賦值一次,變數的值不需要在編譯時期就固定,可以在執行時初始化,一但賦值就不能改變
  • const:宣告常數,編譯時期就固定,且不能再被修改。finalconst 的主要區別在於:final 允許在執行時初始化,而 const 要求在編譯時期就確定值。
  • var:宣告變數,隱式宣告讓 Dart 自行推斷型別,後續可被修改
  • late:延遲一個不為空的變數,確保變數會在第一次使用之前就完成初始化。是一種新的關鍵字,在 Dart 2.12 中引入,可以幫助避免在初始化不為空變數時出現空引用錯誤。
  • async:標記非同步,可搭配 await 等待完成操作
  • await:等待非同步完成操作

總結來說實務上,固定的值使用 const ,例如從伺服器讀取的設定可以使用 final 存放不可變的設定值,用於執行時期才能確定,但之後不會改變的值。late 使用在初始化之後確定有值的情況。

構建 build

當 Widget 要渲染到畫面時會呼叫 build 方法。這個方法會在 initState 和每一次呼叫 setState 之後呼叫。讓我們變更 DestinationWidget 加入按鈕和操作行為:

@override
Widget build(BuildContext context) {
  return Row(
  	children: [
      Text(widget.destinationName),
      Column(
      	children: [
          IconButton(
          	onPressed: () {
              setState(() {
                _counter++;
              });
            },
            icon: const Icon(Icons.thumb_up),
          ),
          Text(_counter.toString()),
        ],
      ),
    ],
  );
}

雖然乍看之下很多程式碼,但如果你分別關注每個 Widget,會發現所有的元素和知識你已經都理解了。

首先,我們使用了 Row ,裡面包含了兩個組件:TextColumnRow 會水平排列包含目的地名稱的文字和 Column。而 Column 也有兩個組件:IconButton 和顯示讚次數的文字,兩者會垂直排列。

最後,IconButton 使用了 onPressed 參數其對應了點擊後的行為,在這個行為中我們使用了 setState 變更了狀態變數,隨後會如我們上面提到的, Flutter 會重新呼叫 build 刷新畫面。

另外 Flutter 內建支援 Material Design 風格的 Icon 組件,上面例子使用了 thumb_upIcon 名稱列表

在專案中使用預設的 Material Design Icon 須在 pubspec.yaml 設定 uses-material-design: true
Flutter 中 Icon 是顯示圖示的 Widget。原理是使用字體圖示,類似於 Web 早期的 FontAwesome 使用字體搭配 Unicode 來實現圖示,字體圖示是一種將本來應該顯示的文字換成圖示的實現方式。(主流的圖示框架現多採用 SVG 的方式實現)。Icon 的範例如下:

Icon(
	Icons.thumb_up,
color: Colors.pink,
size: 24.0,
semanticLabel: '語意化文字'
)

另外,如果要使用其他字體的方式也可以使用 IconData。先設定載入字體:

flutter:
fonts:
    - family: YourIconFontFamily
      fonts:
        - asset: assets/fonts/YourIconFont.ttf

使用 IconData

Icon(IconData(0xe800, fontFamily: 'YourIconFontFamily'))

釋放狀態 dispose

當一個組件從我們的樹狀結構移除的時候會呼叫 dispose() 方法。這個方法通常用於清理 initState() 期間建立所需的基本條件例如:監聽事件,資料庫、網路連線等等。我們會在 dispose() 這個方法解除這些行為。

@override
void dispose() {
  // 清理,解除的程式在這。
  super.dispose();
}

這次 super.dispose() 在最後一行,其他所需的邏輯則是在前面處理。

一般常見的錯誤來源就是在 dispose 未能關閉一些連接,如果沒有關閉一些連線和監聽事件仍會繼續執行嘗試和組件互動進而耗費資源例如記憶體。如果有一個未正確關閉的連線嘗試對已不存在結構中的組件呼叫 setState 將會看到錯誤提示。

掛載 mounted

除了生命週期狀態,還有一個很重要的屬性叫 mounted。這個屬性是用來檢查組件是否仍然掛載在樹狀結構上。具體來說,當一個 State 物件建立且呼叫 initState 之前,Flutter 會通過和BuildContext 關聯掛載這個物件,也就是 initState 呼叫時,mounted 就會被標記為 true。而 dispose 時則變成 false

除非 mountedtrue 否則呼叫 setState 就會發生錯誤。

在實務上,例如在監聽資料庫或網路連接的情況下,我們會用這個屬性來檢查。如果資料庫或網路連線狀態的變化觸發組件更新,那麼後續在呼叫 setState 之前加入一個 mounted 檢查會更加保險。因為有可能在組件移除的過程收到事件。一個簡單的範例如下:

if (mounted) {
  setState(() {
    // 更新狀態的操作
  });
}

通過 mounted 我們可以確保組件仍然還在。到此我們了解了關於組件一些重要的生命週期,也具備足夠的知識處理表單欄位組件的手勢了。

其他生命週期

除了上面實務上比較常用的生命週期之外,另外還有:

  • didChangeDependencies() :這個方法在 initState 執行後立即被呼叫,也會在 State 物件的依賴關係通過 InheritedWidget 發生變化時被呼叫。
  • didUpdateWidget() 當組件更新了新的屬性時呼叫執行,常見的例子就由上層組件通過建構子向子組件傳遞某些變數時。
  • deactivate() 當使用 GlobalKeyState 將組件從一個位置(A 樹狀結構下)移動到另一個位置(B 樹狀結構下)的過程中被調用。

關於生命週期更多的資訊請參考官方文件


上一篇
Day 11 使用者輸入與手勢處理
下一篇
Day 13 表單與欄位
系列文
Flutter 開發實戰 - 30 天逃離新手村38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言