iT邦幫忙

2023 iThome 鐵人賽

DAY 9
1

StatelessStateful widget 是在建構頁面之前,你應該要先仔細思考該頁面是否會有 state 變化的產生而去選擇應該使用的類型。

在 Flutter 中,state 指的是 widget 的狀態或是儲存的資訊,可以藉由控制 state 變化藉此達到改變 widget 樣式、外觀或行為。所以stateless widget 也就代表此 widget 底下不需要進行 state 的處理;stateful widget 則表示需要處理 state 的狀態。以下我們將詳細的介紹

在開始之前,你可以先把 Flutter playground 替換成以下程式碼

import 'package:flutter/material.dart';
void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Playground'),
      ),
      body: Padding(
          padding: const EdgeInsets.all(16.0),
          child:
              Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
            const Text('Hello World',
                style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
            ElevatedButton(
                onPressed: () {},
                child: const Text('翻譯', style: TextStyle(color: Colors.white)))
          ])),
    ),
  ));
}

Widget

在分別的介紹兩者之間,讓我們先來針對前個章節只有引用,但可能不知道箇中細節的 widget 好好認識一番。在 Flutter 的官方定義中這麼說道:

A widget is an immutable decription of a part of a user interface.

從上面這句話我們得到幾個訊息:

  1. Widget 本身為不可變的 (immutable)
  2. Widget 是對介面 (user interface) 的描述 (description)

所以一旦 widget 經過初始化後,便無法再改變。

Stateless Widget

StatelessWidget 表示該 widget 無狀態,通常用於顯示靜態內容,如:文字、圖片、按鈕等等。在我們這幾天建構的應用程式中,其實都是屬於 stateless widget 的範疇,原因是我們還沒有做任何關於互動相關的行為。

讓我們來寫第一個 stateless widget 吧!如果你跟我一樣使用的是 VS Code 並且有安裝插件的話,請在非 void main 函式區域輸入 st 的關鍵字,此時插件就跳出自動補全,其中一個選項為 Flutter Stateless Widget ,請選擇該選項。此時你的 VS Code 應該已經自動插入一段程式碼如下

// 類別名稱為 MyWidget,且該類別繼承 StatelessWidget
class MyWidget extends StatelessWidget {
  // 類別建構子
  const MyWidget({super.key});

  // 由於 StatelessWidget 類別本身有 build 函式,這裡需要進行覆寫
  // build 函式中的回傳值便是當該 Widget 被引用時會顯示的樣貌
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

我們將原先程式碼中 MaterialAPP 中的 home 參數承接的 Scaffold(…) 全數剪下並貼上至 MyWidget 中的 return 值。此時一旦建立 MyWidget 物件便是從 Scaffold 開始往下建構。

此時就 main() 函式便可以簡化成如下:

import 'package:flutter/material.dart';
void main() {
  runApp(const MaterialApp(
    title: 'Flutter Tutorial',
    home: MyWidget(),
  );
}

如此我們就成功的將程式碼以 stateless widget 形式進行封裝。看起來很簡單對吧!

Stateful Widget

前面有說到使用 stateless widget 的時機為顯示靜態內容;相反的如果遇到動態的場景時,就需要 stateful widget 派上用場拉。

由於 Flutter 是藉由偵測 state 狀態是否更新來重新渲染,以達成更新畫面的目的,因此我們需要處理 state 的部分。

讓我們來改寫目前的程式碼。目前為止我們的應用程式執行結果有一個文字顯示「Hello World」 與其下方有一個「翻譯」的按鈕,點擊按鈕什麼事情都還不會發生。我希望可以改成當我點擊「翻譯」的時候,將上方顯示文字改變成「你好,世界」。顯然地,標題的文字顯示內容是可變動的。

因為牽涉到畫面的更新,所以很直觀的這需要透過 stateful widget 來解決。因此讓我們來將原先的 stateless widget 改成 stateful 的版本吧!

VS Code 插件支援快速將 stateless widget 改成 stateful widget 的功能,將滑鼠點至想要更新的 class 上方,會出現一個小燈泡,就可以進行選擇囉
https://i.imgur.com/9NxyE2l.gif

同樣的我們也先來看看這段程式碼,並加入我們所需要狀態管理的變數 title

// 類別名稱為 MyWidget,且該類別繼承 StatefulWidget
class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String title = 'Hello World';
  
  @override
  Widget build(BuildContext context) {
    return ...省略
  }
}

這段程式碼表示幾件事情,MyWidget 已改為繼承 StatefulWidget,並且建立了 state 物件,而 state 物件管理了變數 title 的狀態初始化為 Hello World

當然有時初始化的動作較複雜,如訂閱 Stream 等,在 State 類別中也提供另一個方法 initState() 可供初始化

class _MyWidgetState extends State<MyWidget> {
  late String title;

  @override
  void initState() {
    super.initState();
    title = 'Hello World';
  }
  
  @override
  Widget build(BuildContext context) {
    return ...省略
  }
}

剩下一步,也就是我們要在按鈕點按的時候更新 title 的值。如果要更改 State 的值,則需要透過 setState() 方法來通知 Flutter 說此 widget 的狀態發生了變化,需要重新的渲染。當 widget 需要重新渲染時,便會再一次的呼叫 build 函式。

請找到 button 中的 onPressed() ,並將以下程式碼插入至該函式

// 當有點按事件時,便會觸發此函式
onPressed: () {
  // 需藉由 setState 方法來通知框架此 widget 的狀態有所更動。否則直接改值是不會有效果的
  setState(() {
    message = '你好,世界';
  });
}

這麼一來,每當你點擊「翻譯」按鈕時,便可以成功的將標題替換成想要的文字拉!這也是我們完成的第一個 Stateful widget。

效能淺談

這時可能讀者會開始有疑問了。既然 stateful widget 可根據 state 的變動來決定是否重新 渲染該 widget,也就表示 stateful widget 比較有彈性,那我可不可以所有 widget 都宣告成 stateful widget 呢?

答案是可以的,但是並不建議你這麼做。理由是 stateful widget 在狀態更新時會將自身以及子身底下的子樹也進行重新渲染,當不適當的使用 setState 更新時很可能因過度的重新渲染而造成效能的減損。

因此當在選擇一個 widget 是否要 statueful / stateless 時,

  1. 考慮該 widget 是否有狀態需要記錄,若有才使用 stateful widget;否則一律使用 stateless widget ,因為不必考慮內部狀態變化,建構、重新渲染的過程高效許多。
  2. 避免在太上層的 widget tree 層級使用 stateful widget,因為一旦進行重新渲染時,含該節點及底下的子樹都要一併重新的 rebuild,很明顯也會拖慢效能
  3. 盡可能的縮小 widget 的元件設計,避免明明無需重新渲染卻因 widget 設計在一起被連帶更新的情況

今日總結

其實今天的內容我很糾結是否要與昨天的內容進行順序調換,因為昨天是先教大家引用幾個基礎 widget,但沒有先介紹概念。
但最終仍做了這項決定的原因是希望至少先讓讀者知道 flutter 構建、引用 widget 的語法。然後把改寫成 stateless 與 stateful widget 的實作放在今天,希望有助於大家了解各自的寫法以及選擇應使用何者的使用場景。

今日的總結就一句話:請優先使用 stateless widget ,當有遇到需要記錄內部狀態時才考慮使用 stateful widget。

明天開始我們將要針對我們的專案進行前置作業的設定,我們將用到幾個工具來幫助我們更好的進行開發!


上一篇
[Day 08] Flutter 基礎 widget:Text、Button 與排版元件
下一篇
[Day 10] 實戰新聞 APP - 前置作業 (JSON Server / Vercel / APP 設計稿)
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言