Flutter 應用的*狀態*指的是它用於顯示 UI 或管理系統資源的所有物件。狀態管理是我們組織應用的方式,以便最有效地訪問這些物件並在不同 widget 之間共享它們。

本頁面探討了狀態管理的許多方面,包括:

  • 使用 StatefulWidget
  • 使用建構函式、InheritedWidget 和回撥在 widget 之間共享狀態
  • 使用 Listenable 在有東西改變時通知其他 widget
  • 為你的應用架構使用 Model-View-ViewModel (MVVM)

有關狀態管理的其他介紹,請檢視這些資源:

教程:狀態管理。此教程展示瞭如何將 ChangeNotifierprovider 包一起使用。

本指南不使用 provider 或 Riverpod 等第三方包。相反,它只使用 Flutter 框架中提供的原生功能。

使用 StatefulWidget

#

管理狀態最簡單的方法是使用 StatefulWidget,它將狀態儲存在自身內部。例如,考慮以下 widget:

dart
class MyCounter extends StatefulWidget {
  const MyCounter({super.key});

  @override
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        TextButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text('Increment'),
        )
      ],
    );
  }
}

此程式碼說明了狀態管理中兩個重要的概念:

  • 封裝:使用 MyCounter 的 widget 無法看到底層的 count 變數,也無法訪問或更改它。
  • 物件生命週期_MyCounterState 物件及其 count 變數在 MyCounter 首次構建時建立,並一直存在直到它從螢幕上移除。這是*瞬時狀態*的一個例子。

你可能會發現以下資源很有用:

在 widget 之間共享狀態

#

應用需要儲存狀態的一些場景包括:

  • 更新共享狀態並通知應用的其他部分
  • 監聽共享狀態的變化並在變化時重建 UI

本節探討了如何有效地在應用的不同 widget 之間共享狀態。最常見的模式是:

  • 使用 widget 建構函式(在其他框架中有時稱為“prop drilling”)。
  • 使用 InheritedWidget(或類似的 API,例如 provider 包)。
  • 使用回撥函式通知父 widget 有東西改變了

使用 widget 建構函式

#

由於 Dart 物件是按引用傳遞的,因此 widget 在其建構函式中定義它們需要使用的物件是非常常見的。你傳遞給 widget 建構函式的任何狀態都可以用來構建其 UI。

dart
class MyCounter extends StatelessWidget {
  final int count;
  const MyCounter({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

這使得你的 widget 的其他使用者清楚地知道他們需要提供什麼才能使用它。

dart
Column(
  children: [
    MyCounter(
      count: count,
    ),
    MyCounter(
      count: count,
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        setState(() {
          count++;
        });
      },
    )
  ],
)

透過 widget 建構函式在應用中傳遞共享資料,可以清楚地向任何閱讀程式碼的人表明存在共享依賴。這是一種常見的設計模式,稱為*依賴注入*,許多框架都利用它或提供工具來使其更易於使用。

使用 InheritedWidget

#

手動向下傳遞資料到 widget 樹可能冗長並導致不必要的樣板程式碼,因此 Flutter 提供了 *InheritedWidget*,它提供了一種在父 widget 中高效託管資料的方式,以便子 widget 可以訪問它們而無需將它們作為欄位儲存。

要使用 InheritedWidget,請擴充套件 InheritedWidget 類並使用 dependOnInheritedWidgetOfExactType 實現靜態方法 of()。在構建方法中呼叫 of() 的 widget 會建立一個由 Flutter 框架管理的依賴關係,因此當此 InheritedWidget 使用新資料重新構建並且 updateShouldNotify 返回 true 時,任何依賴於此 InheritedWidget 的 widget 都會重新構建。

dart
class MyState extends InheritedWidget {
  const MyState({
    super.key,
    required this.data,
    required super.child,
  });

  final String data;

  static MyState of(BuildContext context) {
    // This method looks for the nearest `MyState` widget ancestor.
    final result = context.dependOnInheritedWidgetOfExactType<MyState>();

    assert(result != null, 'No MyState found in context');

    return result!;
  }

  @override
  // This method should return true if the old widget's data is different
  // from this widget's data. If true, any widgets that depend on this widget
  // by calling `of()` will be re-built.
  bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}

接下來,在需要訪問共享狀態的 widget 的 build() 方法中呼叫 of() 方法。

dart
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    var data = MyState.of(context).data;
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

使用回撥函式

#

你可以透過公開回調函式來通知其他 widget 值發生了變化。Flutter 提供了 ValueChanged 型別,它聲明瞭一個帶有一個引數的函式回撥。

dart
typedef ValueChanged<T> = void Function(T value);

透過在 widget 的建構函式中公開 onChanged,你為使用此 widget 的任何 widget 提供了一種在你的 widget 呼叫 onChanged 時進行響應的方式。

dart
class MyCounter extends StatefulWidget {
  const MyCounter({super.key, required this.onChanged});

  final ValueChanged<int> onChanged;

  @override
  State<MyCounter> createState() => _MyCounterState();
}

例如,此 widget 可能會處理 onPressed 回撥,並使用其最新的內部 count 變數狀態呼叫 onChanged

dart
TextButton(
  onPressed: () {
    widget.onChanged(count++);
  },
),

深入探究

#

有關在 widget 之間共享狀態的更多資訊,請檢視以下資源:

使用可監聽物件

#

現在你已經選擇瞭如何在應用中共享狀態,那麼當狀態改變時如何更新 UI 呢?你如何以一種通知應用其他部分的方式改變共享狀態呢?

Flutter 提供了一個名為 Listenable 的抽象類,它可以更新一個或多個監聽器。使用可監聽物件的一些有用方式是:

  • 使用 ChangeNotifier 並使用 ListenableBuilder 訂閱它
  • 使用 ValueNotifierValueListenableBuilder

ChangeNotifier

#

要使用 ChangeNotifier,建立一個繼承它的類,並在類需要通知其監聽器時呼叫 notifyListeners

dart
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

然後將其傳遞給 ListenableBuilder,以確保每當 ChangeNotifier 更新其監聽器時,由 builder 函式返回的子樹都會重新構建。

dart
Column(
  children: [
    ListenableBuilder(
      listenable: counterNotifier,
      builder: (context, child) {
        return Text('counter: ${counterNotifier.count}');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.increment();
      },
    ),
  ],
)

ValueNotifier

#

一個 ValueNotifierChangeNotifier 的一個更簡單的版本,它儲存一個單一的值。它實現了 ValueListenableListenable 介面,因此它與 ListenableBuilderValueListenableBuilder 等 widget 相容。要使用它,請建立 ValueNotifier 的例項並提供初始值。

dart
ValueNotifier<int> counterNotifier = ValueNotifier(0);

然後使用 value 欄位讀取或更新值,並通知任何監聽器值已更改。因為 ValueNotifier 擴充套件了 ChangeNotifier,它也是一個 Listenable,可以與 ListenableBuilder 一起使用。但你也可以使用 ValueListenableBuilder,它在 builder 回撥中提供了值。

dart
Column(
  children: [
    ValueListenableBuilder(
      valueListenable: counterNotifier,
      builder: (context, value, child) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

深入探究

#

要了解更多關於 Listenable 物件的資訊,請檢視以下資源:

為你的應用架構使用 MVVM

#

現在我們瞭解瞭如何共享狀態以及當狀態改變時如何通知應用的其他部分,我們準備開始思考如何組織應用中的有狀態物件。

本節描述瞭如何實現一種與 Flutter 等響應式框架配合良好的設計模式,稱為 *Model-View-ViewModel* 或 *MVVM*。

定義模型 (Model)

#

Model 通常是一個 Dart 類,執行低階任務,例如發出 HTTP 請求、快取資料或管理外掛等系統資源。模型通常不需要匯入 Flutter 庫。

例如,考慮一個使用 HTTP 客戶端載入或更新計數器狀態的模型:

dart
import 'package:http/http.dart';

class CounterData {
  CounterData(this.count);

  final int count;
}

class CounterModel {
  Future<CounterData> loadCountFromServer() async {
    final uri = Uri.parse('https://myfluttercounterapp.net/count');
    final response = await get(uri);

    if (response.statusCode != 200) {
      throw ('Failed to update resource');
    }

    return CounterData(int.parse(response.body));
  }

  Future<CounterData> updateCountOnServer(int newCount) async {
    // ...
  }
}

這個模型不使用任何 Flutter 原語,也不對其執行的平臺做任何假設;它唯一的工作是使用其 HTTP 客戶端獲取或更新計數。這允許模型在單元測試中使用 Mock 或 Fake 實現,並定義了應用低階元件和構建完整應用所需的高階 UI 元件之間的清晰界限。

CounterData 類定義了資料的結構,是應用真正的“模型”。模型層通常負責應用所需的核心演算法和資料結構。如果你對定義模型的其他方式感興趣,例如使用不可變值型別,請檢視 pub.dev 上的 freezedbuild_collection 等包。

定義檢視模型 (ViewModel)

#

ViewModel 將*檢視*繫結到*模型*。它保護模型不被檢視直接訪問,並確保資料流從模型的更改開始。資料流由 ViewModel 處理,它使用 notifyListeners 通知檢視有東西改變了。ViewModel 就像餐廳裡的服務員,處理廚房(模型)和顧客(檢視)之間的溝通。

dart
import 'package:flutter/foundation.dart';

class CounterViewModel extends ChangeNotifier {
  final CounterModel model;
  int? count;
  String? errorMessage;
  CounterViewModel(this.model);

  Future<void> init() async {
    try {
      count = (await model.loadCountFromServer()).count;
    } catch (e) {
      errorMessage = 'Could not initialize counter';
    }
    notifyListeners();
  }

  Future<void> increment() async {
    final currentCount = count;
    if (currentCount == null) {
      throw('Not initialized');
    }
    try {
      final incrementedCount = currentCount + 1;
      await model.updateCountOnServer(incrementedCount);
      count = incrementedCount;
    } catch(e) {
      errorMessage = 'Could not update count';
    }
    notifyListeners();
  }
}

請注意,當 ViewModel 從模型接收到錯誤時,它會儲存一個 errorMessage。這可以保護檢視免受未處理的執行時錯誤的影響,這些錯誤可能導致崩潰。相反,檢視可以使用 errorMessage 欄位來顯示使用者友好的錯誤訊息。

定義檢視 (View)

#

由於我們的 ViewModel 是一個 ChangeNotifier,任何引用它的 widget 都可以使用 ListenableBuilder 來在 ViewModel 通知其監聽器時重建其 widget 樹。

dart
ListenableBuilder(
  listenable: viewModel,
  builder: (context, child) {
    return Column(
      children: [
        if (viewModel.errorMessage != null)
          Text(
            'Error: ${viewModel.errorMessage}',
            style: Theme.of(context)
                .textTheme
                .labelSmall
                ?.apply(color: Colors.red),
          ),
        Text('Count: ${viewModel.count}'),
        TextButton(
          onPressed: () {
            viewModel.increment();
          },
          child: Text('Increment'),
        ),
      ],
    );
  },
)

這種模式允許你的應用的業務邏輯與 UI 邏輯和模型層執行的低階操作分離。

瞭解更多關於狀態管理

#

本頁面僅觸及狀態管理的皮毛,因為組織和管理 Flutter 應用狀態的方法有很多。如果你想了解更多,請檢視以下資源:

反饋

#

由於本網站的這一部分正在不斷發展,我們歡迎你的反饋