簡單的應用狀態管理
一種簡單的狀態管理形式。
既然你已經瞭解了宣告式 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 的當前狀態並將新資料應用到其中。用這種方式很難避免錯誤。
在 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<CartModel>。如果你不指定泛型 (<CartModel>),provider 包將無法幫助你。provider 是基於型別的,沒有型別,它就不知道你想要什麼。
Consumer 元件唯一必需的引數是 builder。Builder 是一個函式,每當 ChangeNotifier 發生變化時就會被呼叫。(換句話說,當你呼叫模型中的 notifyListeners() 時,所有相應 Consumer 元件的 builder 方法都會被呼叫。)
Builder 在呼叫時帶有三個引數。第一個是 context,你在每個 build 方法中都能得到它。
builder 函式的第二個引數是 ChangeNotifier 的例項。這正是我們最初請求的物件。你可以使用模型中的資料來定義 UI 在任何給定點應該看起來是什麼樣子。
第三個引數是 child,它是為了最佳化而存在的。如果你在 Consumer 下有一個龐大的元件子樹,且當模型發生變化時該子樹*不會*改變,你可以構建一次並透過 builder 獲取它。
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<CartModel> 來實現,但這會造成浪費。我們會要求框架重建一個實際上不需要重建的元件。
對於這種情況,我們可以使用 Provider.of,並將 listen 引數設定為 false。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上述行,當 notifyListeners 被呼叫時,不會導致該元件重建。
整合所有概念
#你可以檢視本文涵蓋的示例。如果你想要更簡單的內容,請看看當使用 provider 構建簡單的計數器應用時是什麼樣子。
透過學習這些文章,你已經大大提升了建立基於狀態的應用的能力。嘗試自己使用 provider 構建一個應用,以掌握這些技能。