简单的应用状态管理

现在您已经了解了 声明式 UI 编程 以及 临时状态与应用状态 之间的区别,您可以准备了解简单的应用状态管理。

在此页面中,我们将使用 provider 包。如果您是 Flutter 新手,并且没有充分的理由选择其他方法(Redux、Rx、钩子等),那么这可能是您应该开始使用的方法。 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)。换句话说,很难通过调用其上的方法从外部命令式地更改微件。即使你可以让此方法起作用,你也会与框架作斗争,而不是让它帮助你。

// 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)(一个构造函数)。因为你只能在父微件的构建方法中构造新微件,所以如果你想更改 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 微件会消失,并被新的微件完全替换。

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 中,你可以定义以下内容

@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。在复杂应用中,你将有几个模型,因此有几个 ChangeNotifiers。(你根本不需要将 ChangeNotifierprovider 一起使用,但它是一个易于使用的类。)

在我们的购物应用示例中,我们希望在 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 中的所有其他内容都是模型本身及其业务逻辑。

ChangeNotifierflutter: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,这意味着在 MyCartMyCatalog 的上方某处。

您不希望将 ChangeNotifierProvider 放置得比必要的高(因为您不想污染范围)。但在我们的例子中,唯一位于 MyCartMyCatalog 之上的小部件是 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 声明提供给应用中的小部件,我们可以开始使用它了。

这是通过 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,您还可以在每个构建方法中获取它。

构建器函数的第二个参数是 ChangeNotifier 的实例。这正是我们一开始要求的。您可以使用模型中的数据来定义 UI 在任何给定时间点的显示方式。

第三个参数是 child,它用于优化。如果您在 Consumer 下有一个大型组件子树,当模型更改时它不会更改,则可以构建它一次并通过构建器获取它。

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 组件放在树中尽可能深的位置。您不希望仅因为某个地方的一些细节发生了变化而重建 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();

在构建方法中使用上述代码行不会导致在调用 notifyListeners 时重新构建此小组件。

将所有内容放在一起

你可以查看本文中介绍的示例。如果你想要更简单的示例,请参阅使用 provider 构建的简单计数器应用

通过遵循这些文章,你极大地提高了创建基于状态的应用程序的能力。尝试使用 provider 自己构建一个应用程序以掌握这些技能。