跳至主要内容

简单的应用状态管理

现在您已经了解了声明式 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(contents)(构造函数)而不是MyCart.updateWith(somethingNew)(方法调用)。因为您只能在其父级的构建方法中构建新部件,所以如果要更改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。在复杂的应用中,您将拥有多个模型,因此将拥有多个ChangeNotifiers。(您根本不需要在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

#

现在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,您在每个构建方法中也会获得它。

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

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

dart
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 的大块部分。

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,但您仍然需要访问它。例如,“清空购物车”按钮希望允许用户从购物车中删除所有内容。它不需要显示购物车的內容,只需要调用clear()方法。

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

对于此用例,我们可以使用Provider.of,并将listen参数设置为false

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

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

将所有内容整合在一起

#

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

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