既然你已经了解了声明式 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 构建一个应用程序,以掌握这些技能。