既然你已經瞭解了宣告式 UI 程式設計以及瞬態狀態與應用狀態之間的區別,你就可以開始學習簡單的應用狀態管理了。

在本頁面中,我們將使用 provider 包。如果你是 Flutter 新手,並且沒有強烈的理由選擇其他方法(Redux、Rx、hooks 等),那麼這可能是你應該開始嘗試的方法。provider 包易於理解,並且程式碼量不大。它還使用了適用於所有其他方法的概念。

話雖如此,如果你對其他響應式框架的狀態管理有深厚的背景,你可以在選項頁面找到列出的包和教程。

我們的示例

#
An animated gif showing a Flutter app in use. It starts with the user on a login screen. They log in and are taken to the catalog screen, with a list of items. The click on several items, and as they do so, the items are marked as "added". The user clicks on a button and gets taken to the cart view. They see the items there. They go back to the catalog, and the items they bought still show "added". End of animation.

為了說明問題,請考慮以下簡單的應用程式。

該應用有兩個獨立的螢幕:一個目錄和一個購物車(分別由 MyCatalogMyCart 小部件表示)。它可能是一個購物應用,但你可以想象在簡單的社交網路應用中也有相同的結構(將目錄替換為“牆”,將購物車替換為“收藏夾”)。

目錄螢幕包含一個自定義應用欄(MyAppBar)和一個可滾動的許多列表項檢視(MyListItems)。

以下是將應用視覺化為小部件樹。

A widget tree with MyApp at the top, and  MyCatalog and MyCart below it. MyCart area leaf nodes, but MyCatalog have two children: MyAppBar and a list of MyListItems.

因此,我們至少有 5 個 Widget 的子類。其中許多需要訪問“屬於”其他地方的狀態。例如,每個 MyListItem 需要能夠將自己新增到購物車。它可能還想檢視當前顯示的專案是否已在購物車中。

這引出了我們的第一個問題:我們應該把購物車當前的狀態放在哪裡?

狀態提升

#

在 Flutter 中,將狀態儲存在使用它的小部件之上是有意義的。

為什麼?在像 Flutter 這樣的宣告式框架中,如果你想改變 UI,你必須重新構建它。沒有簡單的方法可以實現 MyCart.updateWith(somethingNew)。換句話說,很難透過呼叫方法從外部命令式地改變小部件。即使你能讓它工作,你也會與框架作對,而不是讓它幫助你。

dart
// BAD: DO NOT DO THIS
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

即使你讓上述程式碼工作了,你仍然需要在 MyCart 小部件中處理以下問題:

dart
// 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 的父級或更上級。

dart
// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}

現在 MyCart 只有一個程式碼路徑來構建任何版本的 UI。

dart
// 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 小部件就會消失,並被新的小部件完全替換。

Same widget tree as above, but now we show a small 'cart' badge next to MyApp, and there are two arrows here. One comes from one of the MyListItems to the 'cart', and another one goes from the 'cart' to the MyCart widget.

這就是我們所說的部件是不可變的。它們不會改變——它們會被替換。

現在我們知道購物車狀態放在哪裡了,讓我們看看如何訪問它。

訪問狀態

#

當用戶點選目錄中的某個商品時,該商品會被新增到購物車。但是,由於購物車位於 MyListItem 之上,我們該如何實現呢?

一個簡單的選項是提供一個回撥,當 MyListItem 被點選時可以呼叫它。Dart 的函式是第一類物件,所以你可以隨意傳遞它們。因此,在 MyCatalog 內部,你可以定義以下內容:

dart
@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 的期望,其中一切皆小部件™,這些機制只是特殊型別的小部件——InheritedWidgetInheritedNotifierInheritedModel 等。我們不會在這裡討論這些,因為它們對於我們想要做的事情來說有點低階。

相反,我們將使用一個與底層小部件協同工作但易於使用的包。它叫做 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 中管理購物車狀態。我們建立一個擴充套件它的新類,如下所示:

dart
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 中的其他所有內容都是模型本身及其業務邏輯。

ChangeNotifierflutter:foundation 的一部分,不依賴於 Flutter 中的任何更高級別的類。它很容易測試(你甚至不需要使用小部件測試)。例如,這是一個 CartModel 的簡單單元測試:

dart
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 的情況下,這意味著在 MyCartMyCatalog 之上的某個位置。

你不想把 ChangeNotifierProvider 放在不必要的高層(因為你不想汙染作用域)。但在我們的例子中,唯一同時位於 MyCartMyCatalog 之上的小部件是 MyApp

dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

請注意,我們正在定義一個構建器,它建立 CartModel 的一個新例項。ChangeNotifierProvider 足夠智慧,除非絕對必要,否則不會重建 CartModel。當不再需要該例項時,它還會自動呼叫 CartModel 上的 dispose()

如果您想提供多個類,可以使用 MultiProvider

dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}

Consumer

#

現在,透過頂部的 ChangeNotifierProvider 宣告,CartModel 已提供給我們的應用程式中的小部件,我們可以開始使用它了。

這是透過 Consumer 小部件完成的。

dart
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 下有一個龐大的小部件子樹,並且在模型更改時它不會更改,您可以構建它一次並透過構建器獲取它。

dart
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 的大部分。

dart
// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

反而

dart
// 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

dart
Provider.of<CartModel>(context, listen: false).removeAll();

在構建方法中使用上述程式碼行時,當呼叫 notifyListeners 時,此小部件不會重新構建。

整合所有概念

#

您可以檢視本文涵蓋的示例。如果您想要更簡單的,請參閱使用 provider 構建的簡單計數器應用程式是什麼樣的。

透過閱讀這些文章,您建立基於狀態的應用程式的能力得到了極大的提升。嘗試自己用 provider 構建一個應用程式,以掌握這些技能。