簡單的應用狀態管理
既然你已經瞭解了宣告式 UI 程式設計以及瞬態狀態與應用狀態之間的區別,你就可以開始學習簡單的應用狀態管理了。
在本頁面中,我們將使用 provider 包。如果你是 Flutter 新手,並且沒有強烈的理由選擇其他方法(Redux、Rx、hooks 等),那麼這可能是你應該開始嘗試的方法。provider 包易於理解,並且程式碼量不大。它還使用了適用於所有其他方法的概念。
話雖如此,如果你對其他響應式框架的狀態管理有深厚的背景,你可以在選項頁面找到列出的包和教程。
我們的示例
#
為了說明問題,請考慮以下簡單的應用程式。
該應用有兩個獨立的螢幕:一個目錄和一個購物車(分別由 MyCatalog 和 MyCart 小部件表示)。它可能是一個購物應用,但你可以想象在簡單的社交網路應用中也有相同的結構(將目錄替換為“牆”,將購物車替換為“收藏夾”)。
目錄螢幕包含一個自定義應用欄(MyAppBar)和一個可滾動的許多列表項檢視(MyListItems)。
以下是將應用視覺化為小部件樹。

因此,我們至少有 5 個 Widget 的子類。其中許多需要訪問“屬於”其他地方的狀態。例如,每個 MyListItem 需要能夠將自己新增到購物車。它可能還想檢視當前顯示的專案是否已在購物車中。
這引出了我們的第一個問題:我們應該把購物車當前的狀態放在哪裡?
狀態提升
#在 Flutter 中,將狀態儲存在使用它的小部件之上是有意義的。
為什麼?在像 Flutter 這樣的宣告式框架中,如果你想改變 UI,你必須重新構建它。沒有簡單的方法可以實現 MyCart.updateWith(somethingNew)。換句話說,很難透過呼叫方法從外部命令式地改變小部件。即使你能讓它工作,你也會與框架作對,而不是讓它幫助你。
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}即使你讓上述程式碼工作了,你仍然需要在 MyCart 小部件中處理以下問題:
// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
return SomeWidget(
// The initial state of the cart.
);
}
void updateWith(Item item) {
// Somehow you need to change the UI from here.
}你需要考慮 UI 的當前狀態,並將新資料應用到其中。這種方式很難避免 Bug。
在 Flutter 中,每次內容更改時,你都會構建一個新的小部件。你不是使用 MyCart.updateWith(somethingNew)(方法呼叫),而是使用 MyCart(contents)(建構函式)。因為你只能在其父級的 build 方法中構建新的小部件,所以如果你想更改 contents,它需要存在於 MyCart 的父級或更上級。
// GOOD
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}現在 MyCart 只有一個程式碼路徑來構建任何版本的 UI。
// GOOD
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// Just construct the UI once, using the current state of the cart.
// ···
);
}在我們的示例中,contents 需要存在於 MyApp 中。每當它發生變化時,它就會從上方重建 MyCart(稍後會詳細介紹)。因此,MyCart 無需擔心生命週期——它只需宣告在任何給定 contents 下要顯示的內容。當內容更改時,舊的 MyCart 小部件就會消失,並被新的小部件完全替換。

這就是我們所說的部件是不可變的。它們不會改變——它們會被替換。
現在我們知道購物車狀態放在哪裡了,讓我們看看如何訪問它。
訪問狀態
#當用戶點選目錄中的某個商品時,該商品會被新增到購物車。但是,由於購物車位於 MyListItem 之上,我們該如何實現呢?
一個簡單的選項是提供一個回撥,當 MyListItem 被點選時可以呼叫它。Dart 的函式是第一類物件,所以你可以隨意傳遞它們。因此,在 MyCatalog 內部,你可以定義以下內容:
@override
Widget build(BuildContext context) {
return SomeWidget(
// Construct the widget, passing it a reference to the method above.
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}這還可以,但對於需要從許多不同地方修改的應用狀態,你將不得不傳遞大量的回撥——這很快就會變得很麻煩。
幸運的是,Flutter 有機制讓小部件向其後代(換句話說,不僅僅是其子級,而是其下面的任何小部件)提供資料和服務。正如你對 Flutter 的期望,其中一切皆小部件™,這些機制只是特殊型別的小部件——InheritedWidget、InheritedNotifier、InheritedModel 等。我們不會在這裡討論這些,因為它們對於我們想要做的事情來說有點低階。
相反,我們將使用一個與底層小部件協同工作但易於使用的包。它叫做 provider。
在使用 provider 之前,別忘了在 pubspec.yaml 中新增它的依賴。
要新增 provider 包作為依賴項,請執行 flutter pub add
flutter pub add provider現在你可以 import 'package:provider/provider.dart'; 並開始構建了。
使用 provider,您無需擔心回撥或 InheritedWidgets。但您需要理解 3 個概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
#ChangeNotifier 是 Flutter SDK 中包含的一個簡單類,它向其偵聽器提供更改通知。換句話說,如果某個東西是 ChangeNotifier,你可以訂閱它的更改。(對於熟悉該術語的人來說,它是一種 Observable 形式。)
在 provider 中,ChangeNotifier 是一種封裝應用程式狀態的方式。對於非常簡單的應用程式,一個 ChangeNotifier 就能搞定。在複雜的應用程式中,你將有多個模型,因此有多個 ChangeNotifier。(你根本不需要在 provider 中使用 ChangeNotifier,但它是一個易於使用的類。)
在我們的購物應用示例中,我們希望在 ChangeNotifier 中管理購物車狀態。我們建立一個擴充套件它的新類,如下所示:
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}唯一特定於 ChangeNotifier 的程式碼是對 notifyListeners() 的呼叫。每當模型以可能改變應用程式 UI 的方式更改時,就呼叫此方法。CartModel 中的其他所有內容都是模型本身及其業務邏輯。
ChangeNotifier 是 flutter:foundation 的一部分,不依賴於 Flutter 中的任何更高級別的類。它很容易測試(你甚至不需要使用小部件測試)。例如,這是一個 CartModel 的簡單單元測試:
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
var i = 0;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
i++;
});
cart.add(Item('Dash'));
expect(i, 1);
});ChangeNotifierProvider
#ChangeNotifierProvider 是一個向其後代提供 ChangeNotifier 例項的小部件。它來自 provider 包。
我們已經知道將 ChangeNotifierProvider 放在哪裡:放在需要訪問它的小部件之上。在 CartModel 的情況下,這意味著在 MyCart 和 MyCatalog 之上的某個位置。
你不想把 ChangeNotifierProvider 放在不必要的高層(因為你不想汙染作用域)。但在我們的例子中,唯一同時位於 MyCart 和 MyCatalog 之上的小部件是 MyApp。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}請注意,我們正在定義一個構建器,它建立 CartModel 的一個新例項。ChangeNotifierProvider 足夠智慧,除非絕對必要,否則不會重建 CartModel。當不再需要該例項時,它還會自動呼叫 CartModel 上的 dispose()。
如果您想提供多個類,可以使用 MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}Consumer
#現在,透過頂部的 ChangeNotifierProvider 宣告,CartModel 已提供給我們的應用程式中的小部件,我們可以開始使用它了。
這是透過 Consumer 小部件完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);我們必須指定要訪問的模型的型別。在本例中,我們要訪問 CartModel,因此我們寫 Consumer。如果你不指定泛型 (),provider 包將無法幫助你。provider 是基於型別的,如果沒有型別,它就不知道你想要什麼。
Consumer 小部件唯一必需的引數是構建器。構建器是一個函式,每當 ChangeNotifier 更改時就會呼叫它。(換句話說,當你在模型中呼叫 notifyListeners() 時,所有相應的 Consumer 小部件的所有構建器方法都會被呼叫。)
構建器被呼叫時帶有三個引數。第一個是 context,你在每個構建方法中也會得到它。
構建器函式的第二個引數是 ChangeNotifier 的例項。這正是我們一開始所要求的。你可以使用模型中的資料來定義 UI 在任何給定時間應該是什麼樣子。
第三個引數是 child,它用於最佳化。如果您的 Consumer 下有一個龐大的小部件子樹,並且在模型更改時它不會更改,您可以構建它一次並透過構建器獲取它。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
?child,
Text('Total price: ${cart.totalPrice}'),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);最佳實踐是將您的 Consumer 小部件儘可能深地放置在樹中。您不希望僅僅因為某個細節發生變化就重建 UI 的大部分。
// DON'T DO THIS
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);反而
// DO THIS
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);Provider.of
#有時,您並不真正需要模型中的*資料*來更改 UI,但您仍然需要訪問它。例如,一個 ClearCart 按鈕想要允許使用者從購物車中刪除所有內容。它不需要顯示購物車的內容,它只需要呼叫 clear() 方法。
我們可以為此使用 Consumer,但這會造成浪費。我們將要求框架重建一個不需要重建的小部件。
對於這種用例,我們可以使用 Provider.of,並將 listen 引數設定為 false。
Provider.of<CartModel>(context, listen: false).removeAll();在構建方法中使用上述程式碼行時,當呼叫 notifyListeners 時,此小部件不會重新構建。
整合所有概念
#您可以檢視本文涵蓋的示例。如果您想要更簡單的,請參閱使用 provider 構建的簡單計數器應用程式是什麼樣的。
透過閱讀這些文章,您建立基於狀態的應用程式的能力得到了極大的提升。嘗試自己用 provider 構建一個應用程式,以掌握這些技能。