簡單的應用狀態管理
一種簡單的狀態管理形式。
現在你已經瞭解了宣告式 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,你可以訂閱其更改。(對於熟悉該術語的人來說,它是一種可觀察物件。)
在 provider 中,ChangeNotifier 是封裝你的應用狀態的一種方式。對於非常簡單的應用,你可以使用單個 ChangeNotifier。在複雜的應用中,你將有多個模型,因此會有多個 ChangeNotifiers。(你完全不需要使用 ChangeNotifier 與 provider 一起使用,但它是一個易於使用的類。)
在我們的購物應用示例中,我們希望在 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。它還會自動在不再需要該例項時呼叫 dispose()。
如果你想提供多個類,你可以使用 MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
#現在 CartModel 透過 ChangeNotifierProvider 宣告位於應用的頂部提供給元件,我們可以開始使用它了。
這是透過 Consumer 元件完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我們必須指定我們想要訪問的模型型別。在這種情況下,我們想要 CartModel,因此我們編寫 Consumer<CartModel>。如果你沒有指定泛型(<CartModel>),provider 包將無法幫助你。provider 基於型別,如果沒有型別,它不知道你想要什麼。
Consumer 元件的唯一必需引數是構建器。構建器是一個函式,每當 ChangeNotifier 更改時都會呼叫它。(換句話說,當你呼叫模型中的 notifyListeners() 時,所有相應 Consumer 元件的構建器方法都會被呼叫。)
構建器有三個引數。第一個是 context,你也在每個 build 方法中獲得它。
構建器的第二個引數是 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<CartModel> 來實現這一點,但這將是浪費的。我們會要求框架重建一個不需要重建的元件。
對於這種用例,我們可以使用 Provider.of,並將 listen 引數設定為 false。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上述行不會導致此元件在呼叫 notifyListeners 時重建。
整合所有概念
#你可以 檢視本文中介紹的示例。如果你想要更簡單的示例,請檢視使用 provider 構建的簡單計數器應用 的樣子。
透過跟隨這些文章,你大大提高了建立基於狀態的應用的能力。嘗試自己使用 provider 構建一個應用來掌握這些技能。