簡易應用程式狀態管理
現在您已了解宣告式 UI 程式設計以及暫時性狀態與應用程式狀態之間的差異,您已準備好學習簡單的應用程式狀態管理。
在本頁中,我們將使用 provider
套件。如果您是 Flutter 新手,並且沒有強烈的理由選擇其他方法(Redux、Rx、hooks 等),這可能就是您應該開始使用的方法。provider
套件容易理解,並且不需要太多程式碼。它也使用適用於其他每種方法中的概念。
也就是說,如果您有其他反應式框架的狀態管理豐富經驗,您可以在選項頁面上找到套件和教學。
我們的範例
#為了說明,請考慮以下簡單的應用程式。
該應用程式有兩個獨立的畫面:一個目錄和一個購物車(分別由 MyCatalog
和 MyCart
widget 表示)。它可以是一個購物應用程式,但您可以想像在一個簡單的社群網路應用程式中具有相同的結構(將目錄替換為「牆」,並將購物車替換為「我的最愛」)。
目錄畫面包含一個自訂應用程式列 (MyAppBar
) 和一個包含許多列表項目的滾動視圖 (MyListItems
)。
以下是將應用程式視覺化為 widget 樹狀結構。
因此,我們至少有 5 個 Widget
的子類別。它們中的許多都需要存取「屬於」其他地方的狀態。例如,每個 MyListItem
都需要能夠將自己新增到購物車。它可能還想查看目前顯示的項目是否已在購物車中。
這將引導我們提出第一個問題:我們應該將購物車的目前狀態放在哪裡?
提升狀態
#在 Flutter 中,將狀態保持在使用它的 widget 之上是有道理的。
為什麼?在像 Flutter 這樣的宣告式框架中,如果您想要變更 UI,則必須重建它。沒有簡單的方法可以讓 MyCart.updateWith(somethingNew)
。換句話說,很難透過在 widget 上呼叫方法,以命令式的方式從外部變更 widget。即使您可以使其運作,您也會與框架對抗,而不是讓它幫助您。
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
即使您讓上面的程式碼可以運作,您也必須在 MyCart
widget 中處理以下問題
// 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 中,每次內容變更時,您都會建構一個新的 widget。您使用 MyCart(contents)
(建構函式)而不是 MyCart.updateWith(somethingNew)
(方法呼叫)。因為您只能在其父項的 build 方法中建構新的 widget,如果您想要變更 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
widget 會消失,並完全由新的 widget 取代。
這就是我們所說的 widget 是不可變的含義。它們不會變更 — 它們會被取代。
現在我們知道將購物車的狀態放在哪裡,讓我們看看如何存取它。
存取狀態
#當使用者點擊目錄中的某個項目時,它會被新增到購物車中。但是,由於購物車位於 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 具有機制,可以讓 widget 向其後代提供資料和服務(換句話說,不僅是它們的子項,而且是它們之下的任何 widget)。正如您從 Flutter 中期望的那樣,其中一切都是 Widget™,這些機制只是特殊類型的 widget — InheritedWidget
、InheritedNotifier
、InheritedModel
等。我們不會在此處介紹這些內容,因為它們對於我們想要做的事情來說有點低階。
相反地,我們將使用一個與低階 widget 搭配使用,但易於使用的套件。它稱為 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
。在複雜的應用程式中,您將有多個模型,因此會有幾個 ChangeNotifiers
。(您根本不需要在 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 中的任何較高層級的類別。它很容易測試(您甚至不需要使用widget 測試)。例如,以下是 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
執行個體的 widget。它來自 provider
套件。
我們已經知道將 ChangeNotifierProvider
放在哪裡:在需要存取它的 widget 之上。在 CartModel
的情況下,這表示在 MyCart
和 MyCatalog
之上的某個地方。
您不希望將 ChangeNotifierProvider
放在高於必要的層級(因為您不希望污染範圍)。但在我們的情況下,唯一位於 MyCart
和 MyCatalog
之上的 widget 是 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
#現在 CartModel
已透過頂部的 ChangeNotifierProvider
宣告提供給我們應用程式中的 widget,我們可以開始使用它了。
這是透過 Consumer
widget 完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我們必須指定我們想要存取的模型類型。在這種情況下,我們想要 CartModel
,因此我們寫入 Consumer<CartModel>
。如果您未指定泛型 (<CartModel>
),則 provider
套件將無法協助您。provider
基於類型,並且沒有類型,它不知道您想要什麼。
Consumer
widget 的唯一必要引數是建構器。建構器是一個函式,每當 ChangeNotifier
變更時就會呼叫它。(換句話說,當您在模型中呼叫 notifyListeners()
時,會呼叫所有對應 Consumer
widget 的所有建構器方法。)
建構器會使用三個引數呼叫。第一個引數是 context
,您也可以在每個 build 方法中取得它。
建構函式的第二個引數是 ChangeNotifier
的執行個體。這正是我們首先要求的。您可以使用模型中的資料來定義 UI 在任何給定時間應該是什麼樣子。
第三個引數是 child
,它是用於最佳化。如果您的 Consumer
下有大型 widget 子樹,當模型變更時,該子樹不會變更,則您可以建構它一次並透過建構器取得它。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
if (child != null) child,
Text('Total price: ${cart.totalPrice}'),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);
最佳做法是將您的 Consumer
widget 盡可能放在樹狀結構的深處。您不希望僅僅因為某個地方的某些細節變更而重建大部分 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>
,但這將會浪費資源。我們會要求框架重建不需要重建的 widget。
對於此使用案例,我們可以將 listen
參數設為 false
,來使用 Provider.of
。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上述程式碼行不會導致此 widget 在呼叫 notifyListeners
時重建。
整合所有內容
#您可以查看本文中涵蓋的範例。如果您想要更簡單的內容,請查看使用 provider
建構時,簡單的計數器應用程式是什麼樣子。
透過遵循這些文章,您已大大提高了建立基於狀態的應用程式的能力。嘗試自己使用 provider
建構應用程式,以掌握這些技能。
除非另有說明,否則本網站上的文件反映 Flutter 的最新穩定版本。頁面上次更新於 2024-05-03。 檢視來源 或 回報問題。