狀態管理
Flutter 應用程式的狀態指的是它用來顯示 UI 或管理系統資源的所有物件。狀態管理是指我們如何組織應用程式,以最有效的方式存取這些物件,並在不同的 Widget 之間共享它們。
本頁探討狀態管理的許多面向,包括:
- 使用
StatefulWidget
- 使用建構函式、
InheritedWidget
和回呼在 Widget 之間共享狀態 - 使用
Listenable
在發生變更時通知其他 Widget - 為您的應用程式架構使用模型-視圖-視圖模型 (MVVM)
如需其他狀態管理簡介,請查看以下資源:
- 影片:在 Flutter 中管理狀態。此影片示範如何使用 riverpod 套件。
flutter_dash 教學:狀態管理。此教學示範如何將 ChangeNotifer
與 provider 套件搭配使用。
本指南不使用像是 provider 或 Riverpod 的第三方套件。相反地,它只使用 Flutter 框架中可用的基本元素。
使用 StatefulWidget
#管理狀態最簡單的方法是使用 StatefulWidget
,它會在自身內部儲存狀態。例如,考慮以下 Widget:
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
時建立的,並且會存在到它從螢幕上移除為止。這是短暫狀態的一個例子。
您可能會覺得以下資源很有用:
- 文章:短暫狀態和應用程式狀態
- API 文件:StatefulWidget
在 Widget 之間共用狀態
#應用程式需要儲存狀態的一些情境包括:
- 更新共享狀態並通知應用程式的其他部分
- 監聽共享狀態的變更,並在變更時重建 UI
本節探討如何在應用程式中有效地在不同 Widget 之間共享狀態。最常見的模式是:
- 使用 Widget 建構函式(在其他框架中有時稱為「prop drilling」)
- 使用
InheritedWidget
(或類似的 API,例如 provider 套件)。 - 使用回呼通知父 Widget 發生了變更
使用 Widget 建構函式
#由於 Dart 物件是透過參考傳遞的,因此 Widget 通常會在建構函式中定義它們需要使用的物件。傳遞到 Widget 建構函式中的任何狀態都可用於建構其 UI。
class MyCounter extends StatelessWidget {
final int count;
const MyCounter({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
這讓您 Widget 的其他使用者清楚知道他們需要提供什麼才能使用它
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 時重建。
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()
方法
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
類型,它宣告一個具有單一參數的函式回呼
typedef ValueChanged<T> = void Function(T value);
透過在 Widget 的建構函式中公開 onChanged
,您可以讓任何使用此 Widget 的 Widget 在您的 Widget 呼叫 onChanged
時做出回應。
class MyCounter extends StatefulWidget {
const MyCounter({super.key, required this.onChanged});
final ValueChanged<int> onChanged;
@override
State<MyCounter> createState() => _MyCounterState();
}
例如,此 Widget 可以處理 onPressed
回呼,並使用 count
變數的最新內部狀態呼叫 onChanged
TextButton(
onPressed: () {
widget.onChanged(count++);
},
),
深入探討
#如需有關在 Widget 之間共享狀態的詳細資訊,請查看以下資源:
- 文章:Flutter 架構概觀—狀態管理
- 影片:務實的狀態管理
- 影片:InheritedWidgets
- 影片:Inherited Widget 指南
- 範例:Provider shopper
- 範例:Provider counter
- API 文件:
InheritedWidget
使用可監聽物件
#現在您已經選擇了要在應用程式中共享狀態的方式,當狀態變更時,您該如何更新 UI?您該如何以一種通知應用程式其他部分的方式變更共享狀態?
Flutter 提供一個稱為 Listenable
的抽象類別,它可以更新一個或多個監聽器。一些使用可監聽項目的實用方法是:
- 使用
ChangeNotifier
並使用ListenableBuilder
訂閱它 - 將
ValueNotifier
與ValueListenableBuilder
搭配使用
ChangeNotifier
#若要使用 ChangeNotifier
,請建立一個擴展它的類別,並在類別需要通知其監聽器時呼叫 notifyListeners
。
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
然後將它傳遞給 ListenableBuilder
,以確保每當 ChangeNotifier
更新其監聽器時,builder
函式傳回的子樹狀結構都會重建。
Column(
children: [
ListenableBuilder(
listenable: counterNotifier,
builder: (context, child) {
return Text('counter: ${counterNotifier.count}');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.increment();
},
),
],
)
ValueNotifier
#ValueNotifier
是 ChangeNotifier
的簡化版本,它儲存單一值。它實作了 ValueListenable
和 Listenable
介面,因此它與 ListenableBuilder
和 ValueListenableBuilder
等 Widget 相容。若要使用它,請建立具有初始值的 ValueNotifier
執行個體
ValueNotifier<int> counterNotifier = ValueNotifier(0);
然後使用 value
欄位讀取或更新值,並通知任何監聽器值已變更。由於 ValueNotifier
擴展了 ChangeNotifier
,因此它也是 Listenable
,並且可以與 ListenableBuilder
搭配使用。但是您也可以使用 ValueListenableBuilder
,它會在 builder
回呼中提供值
Column(
children: [
ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, child, value) {
return Text('counter: $value');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.value++;
},
),
],
)
深入探討
#若要深入了解 Listenable
物件,請查看以下資源:
- API 文件:
Listenable
- API 文件:
ValueNotifier
- API 文件:
ValueListenable
- API 文件:
ChangeNotifier
- API 文件:
ListenableBuilder
- API 文件:
ValueListenableBuilder
- API 文件:
InheritedNotifier
針對應用程式的架構使用 MVVM
#現在我們了解如何共享狀態並在狀態變更時通知應用程式的其他部分,我們已經準備好開始思考如何在應用程式中組織有狀態的物件。
本節說明如何實作一種與 Flutter 等反應式框架搭配使用的設計模式,稱為模型-視圖-視圖模型或 MVVM。
定義模型
#模型通常是一個 Dart 類別,它執行低階任務,例如發出 HTTP 要求、快取資料或管理系統資源(例如外掛程式)。模型通常不需要匯入 Flutter 程式庫。
例如,考慮一個使用 HTTP 用戶端載入或更新計數器狀態的模型
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 上的 freezed 或 build_collection 等套件。
定義 ViewModel
#ViewModel
將視圖繫結到模型。它保護模型免於被視圖直接存取,並確保資料流從模型變更開始。資料流由 ViewModel
處理,它使用 notifyListeners
通知視圖發生了變更。ViewModel
就像餐廳裡的服務生,負責處理廚房(模型)和顧客(視圖)之間的通訊。
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
欄位來顯示易於使用的錯誤訊息。
定義檢視
#由於我們的 ViewModel
是 ChangeNotifier
,因此任何參考它的 Widget 都可以使用 ListenableBuilder
,在 ViewModel
通知其監聽器時重建其 Widget 樹狀結構
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 應用程式的狀態。如果您想了解更多資訊,請查看以下資源:
- 文章:狀態管理方法的清單
- 存放庫:Flutter 架構範例
意見回饋
#由於網站的此部分正在發展中,我們歡迎您的意見反應!
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-11-18。檢視原始碼或 回報問題。