Flutter.dev:简单的应用程序状态管理

你好。OTUS在9月推出了新课程Flutter Mobile Developer在课程开始前夕,我们通常会为您准备有用的翻译。








既然您已经了解了声明性用户界面编程以及短暂状态和应用程序状态之间的区别,那么您就可以学习如何轻松管理应用程序状态了。



我们将使用该包provider如果您不熟悉Flutter,并且没有令人信服的理由选择其他方法(Redux,Rx,Hooks等),则这可能是入门的最佳方法。该软件包provider 易于学习,不需要很多代码。他还运用适用于所有其他方法的概念进行操作。



但是,如果您已经具有从其他反应式框架管理状态的丰富经验,则可以查找选项页面上列出的其他软件包和教程









以下面的简单应用程序为例。



该应用程序有两个分开的屏幕:目录和购物车(由小窗口表示的MyCatalogMyCart分别地)。在这种情况下,这是一个购物应用程序,但是您可以想象一个简单的社交网络应用程序中的结构相同(将目录替换为“ wall”,将购物车替换为“ favorites”)。



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



这是小部件树形式的应用程序:







因此,我们至少有5个子类Widget。他们中的许多人需要访问他们不拥有的状态。例如,每个MyListItem应该能够将您自己添加到购物车中。他们可能还需要检查当前显示的物品是否在购物车中。



这将我们引到第一个问题:我们应该将存储桶的当前状态放在哪里?



状况增加



在Flutter中,将状态定位在使用状态的小部件上方是有意义的。



做什么的?在像Flutter这样的声明性框架中,如果要更改用户界面,则必须重新构建它。你不能只是去写东西MyCart.updateWith(somethingNew)换句话说,很难通过在外部调用小部件上的方法来强制从外部更改小部件 即使您可以使用它,您也将在与该框架进行斗争,而不是让它为您提供帮助。



// :   
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}




即使您可以使用上述代码,也必须在小部件中处理MyCart以下内容:



// :   
Widget build(BuildContext context) {
  return SomeWidget(
//   .
  );
}

void updateWith(Item item) {
// -      UI.
}




您将需要考虑UI的当前状态,并将新数据应用于UI。在这里很难避免错误。



在Flutter中,每次其内容更改时都创建一个新的窗口小部件。代替MyCart.updateWith(somethingNew)(方法调用),而使用MyCart(contents)(构造函数)。由于只能在其父代的build方法中创建新的小部件,因此,如果要更改,contents则必须在父代MyCart或更高版本中。



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




现在MyCart只有一个代码路径可以创建任何版本的用户界面。



// 
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    //     ,    .
    // ···
  );
}




在我们的示例中,它contents应该在中MyApp每次更改时,它都会在顶部重建MyCart(稍后再进行介绍)。这种MyCart方式,你不必对生命周期的担心-它只是宣告什么,以示对任何给定的内容。更改后,旧的小部件MyCart将消失,并完全由新的小部件替换。







这就是说小部件是不可变的。它们不会改变-被替换。



既然我们知道将存储桶状态放在何处,让我们看看如何访问它。



国家访问



当用户单击目录中的一项时,它将被添加到购物车中。但是既然购物车结束了MyListItem,我们该怎么做?



一个简单的选项是提供MyListItem可以在单击时调用的回调。 Dart函数是一流的对象,因此您可以根据需要传递它们。因此,在内部,MyCatalog您可以定义以下内容:



@override
Widget build(BuildContext context) {
  return SomeWidget(
   //  ,      .
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}




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



幸运的是,Flutter具有允许小部件向其后代(即,不仅为其后代,还向任何下游小部件)提供数据和服务的机制。正如你对一扑,其中希望一切都是一个Widget,这些机制仅仅是特殊类型的小部件:InheritedWidgetInheritedNotifierInheritedModel等。我们不会在这里描述它们,因为它们与我们试图做的略有出入。



相反,我们将使用与低级窗口小部件一起使用但易于使用的软件包。叫做provider



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



  • 变更通知者
  • ChangeNotifierProvider
  • 消费者




变更通知者



ChangeNotifierFlutter SDK中包含的一个简单类,用于向侦听器提供状态更改通知。换句话说,如果为ChangeNotifier,则可以订阅其更改。 (这是一种可观察的形式-对于不熟悉该术语的人。)



ChangeNotifierInprovider是封装应用程序状态的一种方法。对于非常简单的应用程序,您可以使用one ChangeNotifier。在更复杂的模型中,您将有多个模型,因此会有多个模型ChangeNotifiers。 (你并不需要使用ChangeNotifier与所有provider,但这个类是很容易使用。)



在我们的样本购物应用,我们要管理在车的状态ChangeNotifier。我们创建一个扩展它的新类,例如:



class CartModel extends ChangeNotifier {
///    .
  final List<Item> _items = [];

  ///     .
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  ///      ( ,      42 ).
  int get totalPrice => _items.length * 42;

  ///  [item]  .   [removeAll] -     .
  void add(Item item) {
    _items.add(item);
    //    ,    ,   .
    notifyListeners();
  }

  ///     .
  void removeAll() {
    _items.clear();
    //    ,    ,   .
    notifyListeners();
  }
}




唯一的特定代码段ChangeNotifier是call notifyListeners()每次模型更改时都调用此方法,以使其可以反映在应用程序的UI中。CartModel模型中的其他所有内容都是模型本身及其业务逻辑。



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



test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});




ChangeNotifierProvider



ChangeNotifierProviderChangeNotifier为其子项提供实例的小部件。它以包装形式提供provider



我们已经知道将其放置在哪里ChangeNotifierProvider:需要访问它的小部件上方。万一CartModel暗示上面MyCart上面的东西MyCatalog



您不希望发布的帖子ChangeNotifierProvider超出必要数量(因为您不想污染范围)。但是,在我们的情况下,唯一的构件,在MyCartMyCatalog-它MyApp



void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}




请注意,我们正在定义一个构造器,该构造器将创建一个CartModel. ChangeNotifierProvider足够聪明的新实例CartModel除非绝对必要,否则它不会进行重建当不再需要该实例时,它还会在CartModel上自动调用dispose()。



如果要提供多个类,可以使用MultiProvider



void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}




消费者



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



这是通过小部件完成的Consumer



return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);




我们必须指定我们要访问的模型的类型。在这种情况下,我们需要它CartModel,所以我们写了Consumer<CartModel>。如果您未指定通用(<CartModel>),则该软件包provider将无法为您提供帮助。provider基于类型,没有类型将无法理解您想要的内容。



窗口小部件唯一需要的参数Consumerbuilder。 Builder是在更改时调用的函数ChangeNotifier。 (换句话说,当您调用notifyListeners()模型时,将调用所有相关小部件的所有构建器方法Consumer。)



构造函数由三个参数调用。第一个是context,您还可以在每种构建方法中使用。

builder函数的第二个参数是一个实例ChangeNotifier... 这是我们从一开始就要求的。您可以使用模型数据来确定用户界面在任何给定点的外观。



第三个参数是child,它是优化所必需的。如果您的子树下有一个大型小部件子树Consumer,当模型更改时该子树没有变化,则可以将其构建一次并通过构建器获取。



return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          //   SomeExhibitedWidget,    .
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  //    .
  child: SomeExpensiveWidget(),
);




最好将“消费者”小部件放置在树中尽可能深的位置。您不希望仅由于某些细节已在某处更改而重建用户界面的大部分。



//   
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);




代替这个:



//  
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);




Provider.of



有时您并不需要模型中的数据来更改用户界面,但是您仍然需要访问它。例如,按钮ClearCart允许用户从购物车中删除所有物品。无需显示购物车中的物品,只需调用即可clear()



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



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



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




在build方法中使用上述代码行时,将不会重建此小部件notifyListeners



放在一起



您可以查看本文讨论的示例如果您需要更简单的方法,请查看使用provider创建的简单Counter应用程序的外观



准备好provider自己玩耍时,请记住首先将其依赖项添加到您的pubspec.yaml



name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  # ...




现在你可以了'package:provider/provider.dart'; 并开始建造...






All Articles