iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0

狀態和無狀態組件(Widget)

前面我們已經了解 Widget 在 Flutter 的角色和其重要性 - Every thing is Widget, Widget 作為構成 UI 的一部分,就是描述定義該呈現什麼給使用者的程式碼。

一般來說 UI 會經常變化,如同使用網頁或應用時所體驗到的那樣。儘管根據定義,Widget 是不可變的,但並不表示會維持最終狀態——畢竟 UI 在任何應用的生命週期中肯定會發生變化。這就是為什麼 Flutter 提供了兩種類型的 Widget:無狀態(stateless)和有狀態(stateful)。

無狀態 Widget 不包含狀態,而狀態 Widget 包含狀態且根據狀態進行變更。這個差異會影響 Widget 的生命週期、建構方式以及其程式碼。開發者需要根據需求挑選使用。

在沒有特殊目的的情況下,我們都應該把無狀態 Widget 當作首選,雖然也可以全部都使用狀態 Widget,但會影響效能和可維護性。

不可變性

大多數程式語言的 Immutable 物件表示一物件不會改變。該物件自身無法改變,也不能從外部改變。如果需要改變就是直接取代原來的物件。一個無狀態 Widget 就是一個 Immutable 物件,其不能變更屬性或狀態,也不能通過外部變更,若需要變更則使用一個新的 Widget 取代。

除了上面兩種,Flutter 還有第三種 InheritedWidget

總結 Flutter 支援了三種類型的 Widget:StatefulWidgetStatelessWidgetInheritedWidget

無狀態組件 StatelessWidget

一般來說 UI 通常由多個 Widget 組成,其中一些 Widget 從初始化之後就不會改變其屬性。它們沒有狀態,也就是內部不包含任何可以改變的行為。這些 Widget 的變動由上層的 Widget 觸發事件、變更傳入參數達成改變。也就是說 StatelessWidget 將控制權交給了樹狀結構上層的 Widget 。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
    	// ...
    ),
  }
}

上面範例 MyApp 繼承了 StatelessWidget 並且覆寫了 build() 方法。build 方法對全部 Widget 來說都很重要,此方法定義描述了 Widget 該如何顯示在螢幕上。runApp 傳入的是整個 Widget 樹狀結構的根節點 root。

build 方法中的參數 BuildContex,讓這個組件可以和其他樹狀結構(Widget tree)互動。協助我們存取上層資訊,例如 theme 的資料通常定義在其中,確保所有子組件可以存取。

建立一個組件,當我們繼承 StatelessWidget 時,我們需要 import 引用的套件。一般預設來說我們有 3 種選擇

  • package:flutter/widgets.dart
  • package:flutter/cupertino.dart
  • package:flutter/material.dart

第一個選項包含了所有常用的 Widget 然後這個基礎包也被包含在 material.dartcupertino.dart 中。因此一般來說你只要選擇 material.dartcupertino.dart

為了理解 StatelessWidget 讓我們建立一個新範例

// 範例 1
import 'package:flutter/material.dart';

class DestinationWidget extends StatelessWidget {
  const DestinationWidget({ Key? key }): super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

基於上面的範例我們進一步簡化和加入參數:

// 範例 2
class DestinationWidget extends StatelessWidget {
  DestinationWidget({ require this.name });
  
  final String name;
  
  @override
  Widget build(BuildContext context) {
    return Text(name);
  }
}

關於上面 2 個範例。在第一個範例中我們看到了 KeyKey 是一個可選參數,用於控制 Flutter 框架該如何將元素與底層渲染樹關聯。當我們需要控制重新建構時 - 就是 Widget 是否保留還是重新建立會需要使用 Key。這裡我們嘗試移除了 Keysuper 的部分。

兩個範例建構子的前面分別一個使用 const,一個則無。

const 關鍵字用來標記物件為編譯時期常數。表示編譯時期就確定了不會變。在 Flutter 中使用 const 構建 Widget 可以提高性能,因為它告訴 Flutter 框架這個 Widget 及其所有子 Widget 都是不可變的。

第二個範例,因為我們加入了 name 所以組件是會變動的,無法在編譯時期就固定,因此就不使用 const 了。

狀態組件 StatefulWidget

StatelessWidget 純粹依靠上層組件控制屬性來達成變更不同,StatefulWidget 意味著自己可能動態變更屬性。

雖然根據定義, StatefulWidget 也是不可變,但它們伴隨著一個 State 類別對應當下的狀態。State 物件可以存取 Widget 的屬性。通過額外 State 物件,框架可以在需要的時候重新建置 Widget。

一但 Widget 需要變更時,State 物件會負責通知框架。

  • 一個狀態組件繼承 StatefulWidget 也表示它們需要和一個 State 物件一起工作。

  • StatefulWidget 組件必須覆寫 createState 方法,並且回傳一個 State 物件

  • 通常對應的 State 類別一般會在同一個檔案,為私有的,因為外部不需要直接存取這個狀態類別因此通常會使用 _ 前綴命名,使其為 private。

class MyHomePage extends StatefulWidget {
  MyHomePage({ Key key, required this.title }): super(key: key);
  
  final String title;
  
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

// 在同檔案中對應的 State 物件
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  
  void increment() {
    setState(() {
      _counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
    	appBar: AppBar(
      	title: Text(widget.title)
      ),
      body: Center(
        child: Column(
        	mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Pushed button times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
      	onPress: _increment,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

上面提到狀態 Widget 在其生命週期會隨著狀態異動而改變。而框架該如何知道要重新建置該 Widget 呢?就是 setStatesetState 方法接收一個函式參數並且更新 Widget 對應的狀態。在上面例子我們建立了一個匿名函式並在該函式中 _counter++。接著,框架就會收到通知應使用新的 _counter 重新建置該 Widget。

這是 Flutter 的基礎且需要深入了解。若我們只是更新 _counter 而沒有使用 setState 那麼 Flutter 框架就不會重新渲染 Widget 也就不會看到變更。

當呼叫 setState 時,框架會伴隨著其他行為,呼叫 State 物件中的 build 方法的部分跟前面提到的一樣。不同的是現在我們有狀態。

小結上面範例的流程:

  • MyHomePage 會呼叫對應 State 物件中的 build 方法來呈現介面
  • 當使用者點擊按鈕時 onPressed 會觸發呼叫 _increment 方法
  • _increment 方法呼叫 setState ,並傳入讓 _counter + 1 的匿名函式
  • 框架被通知並執行傳入的匿名函式
  • 框架呼叫對應的 build 達成重新渲染

Dart 中的函式

Dart 的函式非常靈活,支援多種寫法,包括常規函式、箭頭函式、和匿名函式。這些特性讓 Dart 在處理各種情境時更加高效和簡潔。

函式賦值和傳參

Dart 中函式是一級公民也是一種物件,意味著函式可以像其他類型一樣進行賦值和傳參。

void main() {
  // 定義一個函式並賦值給變數
  void func() {
    print('function called');
  }
  
  // 將函式作為參數
  void execute(VoidCallback f) {
    f();
  }
  
  // 呼叫 execute
  execute(func);
}

另外,當函式的主體只需要一行就能完成的時候,可以使用箭頭函式

// 一般函式
String greet(String name) {
  return "Hello, $name.";
}
// 箭頭函式
String greet(String name) => "Hello, $name.";

當需要將函式作為參數傳遞的時候可以如下範例:

void main() {
  String myName = 'andyyou';
  
  String greet(String name) {
    return "Hello, $name";
  }
  
  // 包含一個函式參數的函式
  String runGreet(String name, String Function(String) fn) {
    return fn(name);
  }
  print(runGreet(myName, greet));
}

除了上面的寫法外,還可以使用匿名函式:

print(runGreet(myName, (name) {
  return "Hello, $name.";
}));

// 匿名函式也可用於賦值
// ignore: prefer_function_declarations_over_variables
var callback = () {
  return "Hey";
};

// 或使用箭頭函式
// ignore: prefer_function_declarations_over_variables
var cb = () => "Hi";

繼承組件 InheritedWidget

除了 StatelessWidgetStatefulWidget 之外 Flutter 還有一種類型的 Widget :InheritedWidget 。有時候,Widget 可能需要存取 Widget 樹狀結構外的資料。這種情況下,一種方式是通過中間所有的 Widget 來傳遞資料。

這種方式類似於 React 中利用 props 將資料逐一往下傳遞。

為了解決這個問題,Flutter 提供了 InheritedWidget 。這是一種輔助類型的 Widget,協助傳遞資訊到下層結構。

類似 React 利用 Context 和 Provider 或 Atom 的方式取得資料。

通過加入 InheritedWidget 到樹狀結構中,任何下方的 Widget 可以通過使用 BuildContextof(InheritedWidget) 方法存取其揭露的資料。

該方法接收一個 InheritedWidget 類別作為參數,可在樹狀結構中找到最接近的請求類別物件。

在 Flutter 中 InheritedWidget 最常見的就是 Theme 類別。後續我們會在進一步探討。這裡我們先簡單的提供一個例子:

class ThemeWidget extends InheritedWidget {
  final String themeColor;
  
  ThemeWidget({ required this.themeColor, required Widget child}): super(child: child);
  
  static ThemeWidget of(BuildContext context) {
    final ThemeWidget wiget = context.dependOnInheritedWidgetOfExactType<ThemeWidget>()!;
    // ...
    return wiget;
  }
  
  // ...
}

// 使用!類似 React Provider
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeWidget( // <- 這裡使用
    	themeColor: // ...,
      child: // ...
    );
  }
}

Flutter 中的 Widget 和 Key 屬性

在 Flutter 應用開發中,Widget 是構建介面的基石,而 key 是管理 Widget 狀態和身份的重要工具。這裡我們將深入了解為什麼 key 這麼重要,以及它如何在 Flutter 的框架中發揮作用。

Widget 和 Element Tree

Flutter 框架用 Widget Tree 描述應用的結構和外觀。但是,Widget Tree 本身不負責直接渲染畫面,這項任務是由 Element Tree 完成的。每當 Widget 被實例化,Flutter 都會在 Element Tree 中創建一個對應的元素,這個元素幫助框架理解如何將 Widget 轉換成實際的畫面。

Element Tree 負責追蹤 Widget 的狀態和生命週期。當 Widget 的狀態發生變化時,Element Tree 會決定是否需要重建 Widget,以及如何有效地進行重建。

Key 的作用

key 屬性在這個過程中扮演著關鍵角色。當 Flutter 需要重新構建 Widget 時,key 幫助框架識別哪些 Widget 是新的,哪些是可以重用的。這對於管理狀態和避免不必要的重建非常有幫助。

特別是在有很多相同類型的 Widget 存在時(如一系列的 Text Widget),key 可以幫助 Flutter 正確識別每個 Widget,即使它們的順序發生變化。如果沒有 key,Element Tree 可能無法準確反映 Widget Tree 的變化,導致 UI 表現不符合預期。

大部分的情況下,key 不是必要的,也不應該使用;除非遇到一些奇怪的行為如 Widget 狀態變更,當遇到沒有正確對應 UI 時,那就需要釐清框架是否無法正確認知 Element Tree,還有 key 是否是合適的解法。如果你需要更多關於鍵如何影響 Widget 和可用 key 型別的資訊,請查看官方文檔關於 key 的介紹。

從 React Component 理解 Flutter Widget

如果你熟悉 React,在 React 中 React Component 代表的是 UI ,開發者用來定義結構和管理狀態。React Component 實例化之後產生 React Element 可視為一個描述介面的物件,React Element 包含了渲染需要的內容,但本身不是真正的 DOM 節點。最終 React 渲染引擎使用 React Element 轉換成 DOM 節點在網頁上顯示內容。

Flutter Widget 和 React Component 類似,開發者用 Widget 描述介面,定義結構。進而組成 Widget Tree,每個 Widget 物件實例會產生對應的 Element,Element 管理追蹤 Widget 的生命週期,狀態,和重建。最終 Flutter 渲染引擎使用 Element 轉換為 Render Object 輸出實際畫面。

大體上兩者具有類似的概念,但是 Flutter 的 Element 具體涉及到更多 Widget 的狀態與生命週期,而 React Element 則是渲染的輕量級描述。

是否可自由實現 UI 設計

在「快速入門」的章節我們看到了 Flutter 支援了 MaterialApp 和 CupertinoApp 兩種風格的組件,感覺風格已經確立了,那是否我們能夠自由的實現 UI 設計呢?所謂的彈性是否足夠?

雖然 Flutter 確實提供了許多預設的 Material Design 和Cupertino 風格的組件,但這些只是起點,也只是方便您的開發。

以下是一些 Flutter 如何支持自由介面設計的方法:

自定義Widge

您可以從頭開始創建自己的Widget,不使用任何預設組件。

class CustomButton extends StatelessWidget {
  final VoidCallback onPressed;
  final String text;

  CustomButton({required this.onPressed, required this.text});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onPressed,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
        decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(30),
        ),
        child: Text(text, style: TextStyle(color: Colors.white)),
      ),
    );
  }
}

調整現有 Widget

ElevatedButton(
  style: ElevatedButton.styleFrom(
    primary: Colors.green,
    onPrimary: Colors.white,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(30),
    ),
    padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
  ),
  child: Text('自定義按鈕'),
  onPressed: () {},
)

除此之外,還可以使用進階的 CustomPainter 自定義繪製,使用主題,覆寫組件屬性,甚至還有大企業提供的 UI 函式庫例如 騰訊 TDesign

這篇文章深入探討了 Flutter 中的 Widget 概念和狀態管理。主要包含以下幾個方面:

  1. Widget 的類型:介紹了無狀態(StatelessWidget)、有狀態(StatefulWidget)和繼承(InheritedWidget)三種主要的 Widget 類型。
  2. 不可變性:解釋了 Flutter 中 Widget 的不可變性概念,以及如何通過創建新的 Widget 來實現 UI 更新。
  3. StatelessWidget 和 StatefulWidget:詳細說明了這兩種 Widget 的特性、使用場景和生命週期。
  4. Dart 函數:介紹了 Dart 語言中函數的靈活性,包括函數賦值、傳參和箭頭函數等特性。
  5. InheritedWidget:解釋了如何使用 InheritedWidget 在 Widget 樹中共享數據。
  6. Widget 和 Element Tree:描述了 Flutter 框架如何使用 Widget Tree 和 Element Tree 來管理 UI 結構和渲染。
  7. Key 的重要性:討論了 Key 在 Widget 管理和重建過程中的關鍵作用。
  8. 自由的 UI 設計:最後,文章強調了 Flutter 框架的靈活性,說明了開發者如何通過自定義 Widget、調整現有 Widget 等方式實現自由的 UI 設計。

總結來說,我們介紹了 Flutter 中 Widget 的核心概念和使用方法,提供了理解 Flutter UI 開發的基礎知識。


上一篇
Day 8 Dart 深入探索 Flutter 常用特性
下一篇
Day 10 認識內建組件 Widgets
系列文
Flutter 開發實戰 - 30 天逃離新手村27
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言