Skip to main content

简单应用状态管理

现在您已经了解了声明式 UI 编程以及短暂状态和应用状态之间的区别,您就可以学习简单的应用状态管理了。

在本页中,我们将使用provider包。如果您是 Flutter 新手,并且没有充分理由选择其他方法(Redux、Rx、hooks 等),那么这可能是您应该首先尝试的方法。provider包易于理解,而且代码量不多。它还使用了适用于其他所有方法的概念。

也就是说,如果您在其他反应式框架中拥有丰富的状态管理经验,您可以在选项页面上找到列出的包和教程。

示例

#
一个动画gif显示一个Flutter应用程序的使用情况。它从用户登录屏幕开始。他们登录并进入产品目录屏幕,其中列出了商品。他们点击几件商品,当他们这样做时,这些商品会被标记为“已添加”。用户点击一个按钮,进入购物车视图。他们在那里看到商品。他们返回产品目录,他们购买的商品仍然显示为“已添加”。动画结束。

为了说明,请考虑以下简单的应用程序。

该应用程序有两个单独的屏幕:产品目录和购物车(分别由MyCatalogMyCart小部件表示)。它可以是一个购物应用程序,但您可以在简单的社交网络应用程序中想象相同的结构(将产品目录替换为“墙”,将购物车替换为“收藏夹”)。

产品目录屏幕包括自定义应用程序栏(MyAppBar)和许多列表项的滚动视图(MyListItems)。

以下是可视化的应用程序作为小部件树。

一个顶层为MyApp的小部件树,下方为MyCatalog和MyCart。MyCart是叶子节点,但MyCatalog有两个子节点:MyAppBar和一个MyListItems列表。

所以我们至少有5个Widget的子类。它们中的许多都需要访问“属于”其他地方的状态。例如,每个MyListItem都需要能够将自身添加到购物车。它可能还想查看当前显示的商品是否已在购物车中。

这引出了我们的第一个问题:我们应该将购物车的当前状态放在哪里?

上移状态

#

在 Flutter 中,将状态保存在使用它的 Widget 之上是有意义的。

为什么?在像 Flutter 这样的声明式框架中,如果要更改 UI,则必须重建它。没有简单的方法可以执行MyCart.updateWith(somethingNew)。换句话说,很难通过调用其上的方法来强制更改外部的小部件。即使您可以使此方法有效,您也会与框架对抗,而不是让它帮助您。

dart
// 错误:不要这样做
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

即使您使上述代码有效,您还必须在MyCart小部件中处理以下内容:

dart
// 错误:不要这样做
Widget build(BuildContext context) {
  return SomeWidget(
    // 购物车的初始状态。
  );
}

void updateWith(Item item) {
  // 以某种方式,您需要从此处更改UI。
}

您需要考虑 UI 的当前状态并将新数据应用于它。很难避免这种方式的错误。

在 Flutter 中,每次其内容发生更改时,您都会构造一个新的小部件。您使用MyCart(contents)(构造函数)而不是MyCart.updateWith(somethingNew)(方法调用)。因为您只能在其父级的构建方法中构造新的小部件,所以如果要更改contents,它需要位于MyCart的父级或更高位置。

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

现在MyCart只有一个构建任何 UI 版本的代码路径。

dart
// 正确
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // 只需使用购物车的当前状态构造一次UI。
    // ···
  );
}

在我们的示例中,contents需要位于MyApp中。每当它发生更改时,它都会从上方重建MyCart(稍后会详细介绍)。因此,MyCart不需要担心生命周期——它只声明在任何给定的contents中显示的内容。当它发生更改时,旧的MyCart小部件会消失,并被新的完全替换。

与上面相同的小部件树,但现在我们在MyApp旁边显示了一个小的'购物车'徽章,这里有两条箭头。一条来自MyListItems之一到'购物车',另一条从'购物车'到MyCart小部件。

这就是我们所说的 Widget 不可变的意思。它们不会改变——它们会被替换。

现在我们知道将购物车的状态放在哪里了,让我们看看如何访问它。

访问状态

#

当用户点击产品目录中的其中一件商品时,它会被添加到购物车中。但由于购物车位于MyListItem之上,我们该如何做到这一点呢?

一个简单的选项是提供一个回调,MyListItem在被点击时可以调用该回调。Dart 的函数是一等公民对象,因此您可以按任何方式传递它们。因此,在MyCatalog中,您可以定义以下内容:

dart
@override
Widget build(BuildContext context) {
  return SomeWidget(
    // 构造小部件,将对上面方法的引用传递给它。
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('用户点击了 $item');
}

这可以正常工作,但是对于需要从许多不同位置修改的应用程序状态,您必须传递许多回调——这很快就会变得很麻烦。

幸运的是,Flutter 具有允许小部件向其后代(换句话说,不仅仅是其子级,而是它们下面的任何小部件)提供数据和服务的机制。正如您对 Flutter 的预期一样,万物皆为 Widget™,这些机制只是特殊类型的小部件——InheritedWidgetInheritedNotifierInheritedModel等等。我们不会在这里介绍这些内容,因为对于我们正在尝试做的事情来说,它们有点低级。

相反,我们将使用一个与低级小部件一起工作但易于使用的包。它被称为provider

在使用provider之前,不要忘记在您的pubspec.yaml中添加对它的依赖。

要添加provider包作为依赖项,请运行flutter pub add

flutter pub add provider

现在您可以import 'package:provider/provider.dart';并开始构建。

使用provider,您无需担心回调或InheritedWidgets。但是您需要理解三个概念:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer

ChangeNotifier

#

ChangeNotifier是Flutter SDK中包含的一个简单类,它向其侦听器提供更改通知。换句话说,如果某些内容是ChangeNotifier,您可以订阅其更改。(对于熟悉此术语的人来说,它是一种 Observable。)

provider中,ChangeNotifier是一种封装应用程序状态的方法。对于非常简单的应用程序,您可以使用单个ChangeNotifier。在复杂的应用程序中,您将拥有多个模型,因此将拥有多个ChangeNotifiers。(您根本不需要在provider中使用ChangeNotifier,但它是一个易于使用的类。)

在我们的购物应用程序示例中,我们希望在一个ChangeNotifier中管理购物车的状态。我们创建一个扩展它的新类,如下所示:

dart
class CartModel extends ChangeNotifier {
  /// 购物车的内部私有状态。
  final List<Item> _items = [];

  /// 购物车中商品的不可修改视图。
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// 所有商品的当前总价(假设所有商品的价格均为 42 美元)。
  int get totalPrice => _items.length * 42;

  /// 将[item]添加到购物车。这是从外部修改购物车的唯一方法。
  void add(Item item) {
    _items.add(item);
    // 此调用告诉正在侦听此模型的小部件重建。
    notifyListeners();
  }

  /// 从购物车中移除所有商品。
  void removeAll() {
    _items.clear();
    // 此调用告诉正在侦听此模型的小部件重建。
    notifyListeners();
  }
}

唯一特定于ChangeNotifier的代码是对notifyListeners()的调用。每当模型以可能更改应用程序 UI 的方式更改时,都调用此方法。CartModel中的其他所有内容都是模型本身及其业务逻辑。

ChangeNotifierflutter:foundation的一部分,不依赖于 Flutter 中任何更高级别的类。它易于测试(您甚至不需要为此使用小部件测试)。例如,以下是CartModel的简单单元测试:

dart
test('添加商品会增加总成本', () {
  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放在哪里:在需要访问它的 Widget 之上。对于CartModel来说,这意味着位于MyCartMyCatalog之上的某个位置。

您不希望将ChangeNotifierProvider放置得比必要的高(因为您不希望污染范围)。但在我们的例子中,唯一位于MyCartMyCatalog之上的 Widget 是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

#

现在CartModel已经通过顶部的ChangeNotifierProvider声明提供给应用程序中的小部件,我们可以开始使用它了。

这是通过Consumer小部件完成的。

dart
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text('总价:${cart.totalPrice}');
  },
);

我们必须指定要访问的模型的类型。在本例中,我们想要CartModel,因此我们编写Consumer<CartModel>。如果不指定泛型(<CartModel>),provider包将无法帮助您。provider基于类型,如果没有类型,它不知道您想要什么。

Consumer小部件唯一需要的参数是构建器。构建器是一个函数,每当ChangeNotifier发生更改时都会调用该函数。(换句话说,当您在模型中调用notifyListeners()时,所有相应Consumer小部件的所有构建器方法都会被调用。)

构建器使用三个参数调用。第一个是context,您在每个构建方法中也会得到它。

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

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

dart
return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // 在这里使用SomeExpensiveWidget,无需每次都重建。
      if (child != null) child,
      Text('总价:${cart.totalPrice}'),
    ],
  ),
  // 在这里构建昂贵的小部件。
  child: const SomeExpensiveWidget(),
);

最佳实践是将您的Consumer小部件尽可能地放在树的深处。您不希望仅仅因为某个地方的一些细节发生了更改而重建 UI 的大部分内容。

dart
// 不要这样做
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('总价:${cart.totalPrice}'),
      ),
    );
  },
);

改为:

dart
// 做这个
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('总价:${cart.totalPrice}');
      },
    ),
  ),
);

Provider.of

#

有时,您并不真正需要模型中的 数据 来更改 UI,但您仍然需要访问它。例如,ClearCart按钮允许用户从购物车中移除所有内容。它不需要显示购物车的全部内容,它只需要调用clear()方法。

我们可以为此使用Consumer<CartModel>,但这将是浪费的。我们将要求框架重建不需要重建的小部件。

对于这种情况,我们可以使用Provider.of,并将listen参数设置为false

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

在构建方法中使用以上代码不会在调用notifyListeners时导致此小部件重建。

总结

#

您可以查看本文中介绍的示例。如果您想要更简单的示例,请查看使用provider构建的简单计数器应用程序的外观使用provider构建

通过阅读这些文章,您已经大大提高了创建基于状态的应用程序的能力。尝试自己使用provider构建一个应用程序来掌握这些技能。