跳至主要內容

狀態管理

Flutter 應用程式的狀態指的是它用來顯示 UI 或管理系統資源的所有物件。狀態管理是指我們如何組織應用程式,以最有效的方式存取這些物件,並在不同的 Widget 之間共享它們。

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

  • 使用 StatefulWidget
  • 使用建構函式、InheritedWidget 和回呼在 Widget 之間共享狀態
  • 使用 Listenable 在發生變更時通知其他 Widget
  • 為您的應用程式架構使用模型-視圖-視圖模型 (MVVM)

如需其他狀態管理簡介,請查看以下資源:

教學:狀態管理。此教學示範如何將 ChangeNotiferprovider 套件搭配使用。

本指南不使用像是 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 的 Widget 在此 Widget 使用新資料重建且 updateShouldNotify 傳回 true 時重建。

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, child, value) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

深入探討

#

若要深入了解 Listenable 物件,請查看以下資源:

針對應用程式的架構使用 MVVM

#

現在我們了解如何共享狀態並在狀態變更時通知應用程式的其他部分,我們已經準備好開始思考如何在應用程式中組織有狀態的物件。

本節說明如何實作一種與 Flutter 等反應式框架搭配使用的設計模式,稱為模型-視圖-視圖模型MVVM

定義模型

#

模型通常是一個 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 {
    var count = this.count;
    if (count == null) {
      throw('Not initialized');
    }
    try {
      await model.updateCountOnServer(count + 1);
      count++;
    } catch(e) {
      errorMessage = 'Count not update count';
    }
    notifyListeners();
  }
}

請注意,當 ViewModel 從模型接收到錯誤時,它會儲存一個 errorMessage。這可保護視圖免於未處理的執行階段錯誤,這可能會導致當機。相反地,視圖可以使用 errorMessage 欄位來顯示易於使用的錯誤訊息。

定義檢視

#

由於我們的 ViewModelChangeNotifier,因此任何參考它的 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 應用程式的狀態。如果您想了解更多資訊,請查看以下資源:

意見回饋

#

由於網站的此部分正在發展中,我們歡迎您的意見反應