跳到主内容

简单的应用状态管理

一种简单的状态管理形式。

现在你已经了解了声明式 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 构建一个应用来掌握这些技能。