跳到主內容

簡單的應用狀態管理

一種簡單的狀態管理形式。

現在你已經瞭解了宣告式 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 的當前狀態並將新資料應用到它。這樣很難避免錯誤。

在 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,你可以訂閱其更改。(對於熟悉該術語的人來說,它是一種可觀察物件。)

provider 中,ChangeNotifier 是封裝你的應用狀態的一種方式。對於非常簡單的應用,你可以使用單個 ChangeNotifier。在複雜的應用中,你將有多個模型,因此會有多個 ChangeNotifiers。(你完全不需要使用 ChangeNotifierprovider 一起使用,但它是一個易於使用的類。)

在我們的購物應用示例中,我們希望在 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。它還會自動在不再需要該例項時呼叫 dispose()

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

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

Consumer

#

現在 CartModel 透過 ChangeNotifierProvider 宣告位於應用的頂部提供給元件,我們可以開始使用它了。

這是透過 Consumer 元件完成的。

dart
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 下方有一個大型元件子樹,該子樹在模型更改時不會更改,你可以一次構建它並透過構建器獲取它。

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<CartModel> 來實現這一點,但這將是浪費的。我們會要求框架重建一個不需要重建的元件。

對於這種用例,我們可以使用 Provider.of,並將 listen 引數設定為 false

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

在 build 方法中使用上述行不會導致此元件在呼叫 notifyListeners 時重建。

整合所有概念

#

你可以 檢視本文中介紹的示例。如果你想要更簡單的示例,請檢視使用 provider 構建的簡單計數器應用 的樣子

透過跟隨這些文章,你大大提高了建立基於狀態的應用的能力。嘗試自己使用 provider 構建一個應用來掌握這些技能。