当前位置:   article > 正文

Flutter状态管理

Flutter状态管理

状态管理中的声明式编程思维

Flutter应用是 声明式 的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态,在 Flutter 应用中,当状态变化时,会重新构建部分界面,而不是原生Android或iOS的命令式。当Flutter应用的状态发生改变时(例如:点击了一个按钮,触发了某个动画或者某个值的更新),改变状态就会导致UI界面重绘。去改变用户界面本身是没有必要的(例如 widget.setText ),因为这样的代码不会在UI界面上更新,只要改变了状态,那么用户界面将重新构建。

通过setState更新

  1. class MyHomePage extends StatefulWidget {
  2. const MyHomePage({super.key, required this.title});
  3. final String title;
  4. @override
  5. State<MyHomePage> createState() => _MyHomePageState();
  6. }
  7. class _MyHomePageState extends State<MyHomePage> {
  8. int _counter = 0;
  9. void _incrementCounter() {
  10. setState(() {
  11. _counter++;
  12. });
  13. }
  14. @override
  15. Widget build(BuildContext context) {
  16. return Scaffold(
  17. appBar: AppBar(
  18. title: Text(widget.title),
  19. ),
  20. body: Center(
  21. child: Column(
  22. mainAxisAlignment: MainAxisAlignment.center,
  23. children: <Widget>[
  24. const Text(
  25. 'You have pushed the button this many times:',
  26. ),
  27. Text(
  28. '$_counter',
  29. style: Theme.of(context).textTheme.headlineMedium,
  30. ),
  31. ],
  32. ),
  33. ),
  34. floatingActionButton: FloatingActionButton(
  35. onPressed: _incrementCounter,
  36. tooltip: 'Increment',
  37. child: const Icon(Icons.add),
  38. ), // This trailing comma makes auto-formatting nicer for build methods.
  39. );
  40. }
  41. }

从上述代码可以看出,如果我们需要改变某个值,只需要使用 setState((){ }) 在此函数中更新就会导致界面重绘,会重新执行build构建UI,前提是此Widget是StatefulWidget的子类。

State方式的状态属于短暂状态,widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变,需要用的只是一个 StatefulWidget

了解Provider

如果我们想在应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态),为了管理应用状态,就需要研究使用Provider。

示例

假设我们有2个页面,A页面编辑的信息,在B页面需要使用,那此时我们可能无法在B中使用State拿到A页面的信息,此时Provider就帮上忙了,需要访问一些全局的状态。比如,A页面的会被添加到B页面中。但是它可能需要检查和自己相同的元素是否已经被添加到B页面中。

这里我们出现了第一个问题:我们把当前页面的状态放在哪合适呢?

提高状态的层级

在 Flutter 中,有必要将存储状态的对象置于 widget 树中对应 widget 的上层。

为什么呢?在类似 Flutter 的声明式框架中,如果你想要修改 UI,那么你需要重构它。并没有类似 B.updateWith(newData) 的简单调用方法。很难通过外部调用方法修改一个 widget。即便自己实现了这样的模式,那也是和整个框架不相兼容。

比如在B页面的入口在A页面,那需要在A页面创建Provider,在B中可以共享到A页面创建的 数据,更新数据后A也能同步到最新的数据。

  1. void onTap(BuildContext context) {
  2. var model = ProviderModel(context);
  3. model.add(item);
  4. }

这里 B页面 可以在各种版本的 UI 中调用同一个代码路径,获取数据

  1. Widget build(BuildContext context) {
  2. var model = ProviderModel(context);
  3. return Continer(
  4. // ···
  5. );
  6. }

在上面的例子中,model会存在于A-B 的生命周期中。当它发生改变的时候,它会从上层重构 B页面 。因为这个机制,所以 B页面 无需考虑生命周期的问题—它只需要针对 providerModel声明所需显示内容即可。当内容发生改变的时候,旧的 B的 widget 就会消失,完全被新的 widget 替代。

如何使用

在使用 provider 之前,请不要忘记在 pubspec.yaml 文件里加入依赖。

运行 flutter pub add provider 添加为依赖:

flutter pub add provider

现在可以在代码里加入 import 'package:provider/provider.dart'; 进而开始构建你的应用了

provider 你无须关心回调或者 InheritedWidgets。但是你需要理解三个概念:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer
ChangeNotifier

ChangeNotifier 是 Flutter SDK 中的一个简单的类。它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。(这和大家所熟悉的观察者模式相类似)。

在 provider 中,ChangeNotifier 是一种能够封装应用程序状态的方法。对于特别简单的程序,你可以通过一个 ChangeNotifier 来满足全部需求。在相对复杂的应用中,由于会有多个模型,所以可能会有多个 ChangeNotifier。 (不是必须得把 ChangeNotifier 和 provider 结合起来用,不过它确实是一个特别简单的类)。

在示例中用 ChangeNotifier 来管理状态。我们创建一个新类,继承它(可以把他理解成MVVM中的viewModel),像下面这样:

  1. class ProviderModel extends ChangeNotifier {
  2. /// Internal, private state of the cart.
  3. final List<Item> _items = [];
  4. /// An unmodifiable view of the items in the cart.
  5. UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
  6. /// The current total price of all items (assuming all items cost $42).
  7. int get totalPrice => _items.length * 42;
  8. /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  9. /// cart from the outside.
  10. void add(Item item) {
  11. _items.add(item);
  12. // This call tells the widgets that are listening to this model to rebuild.
  13. notifyListeners();
  14. }
  15. /// Removes all items from the cart.
  16. void removeAll() {
  17. _items.clear();
  18. // This call tells the widgets that are listening to this model to rebuild.
  19. notifyListeners();
  20. }
  21. }

唯一一行和 ChangeNotifier 相关的代码就是调用 notifyListeners()。当模型发生改变并且需要更新 UI 的时候可以调用该方法。而剩下的代码就是 ProviderModel 和它本身的业务逻辑。可以这么理解,调用了 notifyListeners()之后,会执行创建Provider的Build方法重新构建UI

如果使用创建的Provider:

  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. // This widget is the root of your application.
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. title: 'Flutter Demo',
  8. theme: ThemeData(
  9. primarySwatch: Colors.blue,
  10. ),
  11. home: ChangeNotifierProvider(
  12. create: (context) => ProviderModel(), child: MyHomePage(title: 'Flutter Demo Home Page')),
  13. );
  14. }
  15. }

或者

  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. // This widget is the root of your application.
  4. @override
  5. Widget build(BuildContext context) {
  6. return MaterialApp(
  7. title: 'Flutter Demo',
  8. theme: ThemeData(
  9. primarySwatch: Colors.blue,
  10. ),
  11. home: ChangeNotifierProvider(
  12. create: (context) => ProviderModel(),
  13. child: Consumer<ProviderModel>(builder: (context, viewModel, child) {
  14. return MyHomePage(title: 'Flutter Demo Home Page');
  15. })));
  16. }
  17. }

现在 ProviderModel 已经通过 ChangeNotifierProvider 在应用中与 widget 相关联。我们可以开始调用它了。

 child: Consumer<ProviderModel>(builder: (context, viewModel, child) {}

我们必须指定要访问的模型类型。在这个示例中,我们要访问 ProviderModel 那么就写上 Consumer<ProviderModel>

Consumer widget 唯一必须的参数就是 builder。当 ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当你在模型中调用 notifyListeners() 时,所有相关的 Consumer widget 的 builder 方法都会被调用。)

builder 在被调用的时候会用到三个参数。第一个是 context,在每个 build 方法中都能找到这个参数。

builder 函数的第二个参数是 ChangeNotifier 的实例。它是我们最开始就能得到的实例。你可以通过该实例定义 UI 的内容

第三个参数是 child,用于优化目的。如果 Consumer 下面有一个庞大的子树,当模型发生改变的时候,该子树 并不会 改变,那么你就可以仅仅创建它一次,然后通过 builder 获得该实例。

  1. return Consumer<ProviderModel>(
  2. builder: (context, cart, child) => Stack(
  3. children: [
  4. // Use SomeExpensiveWidget here, without rebuilding every time.
  5. if (child != null) child,
  6. Text('Total price: ${cart.totalPrice}'),
  7. ],
  8. ),
  9. // Build the expensive widget here.
  10. child: const SomeExpensiveWidget(),
  11. );

如果我们Widget树比较庞大,那么就需要考虑到,更换Consumer的位置,在需要更新的位置使用,这样当数据发生改变时就不会全盘重新构建 widget 树了。例如:

  1. return Consumer<ProviderModel>(
  2. builder: (context, cart, child) {
  3. return AWidget(
  4. // ...
  5. child: BWidget(
  6. // ...
  7. child: Text('Total price: ${cart.totalPrice}'),
  8. ),
  9. );
  10. },
  11. );

换成:

  1. return AWidget(
  2. // ...
  3. child: Consumer<ProviderModel>(
  4. builder: (context, cart, child) {
  5. return BWidget(
  6. // ...
  7. child: Text('Total price: ${cart.totalPrice}'),
  8. ),
  9. );
  10. },
  11. );

有的时候你不需要模型中的 数据 来改变 UI,但是你可能还是需要访问该数据。比如A页面的一个按钮能够B页面的数据。它不需要显示B页面里的内容,只需要调用 clear() 方法。

我们可以使用 Consumer<ProviderModel> 来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget,所以这里我们可以使用 Provider.of,并且将 listen 设置为 false

可以使用:

Provider.of<ServicePieceViewModel>(context, listen: false).clear();

或许通过context:

context.read<ServicePieceViewModel>().clear();

在 build 方法中使用上面的代码,当 notifyListeners 被调用的时候,并不会使 widget 被重构。

context.read和context.watch的区别

  1. extension ReadContext on BuildContext {
  2. T read<T>() {
  3. return Provider.of<T>(this, listen: false);
  4. }
  5. }
  1. extension WatchContext on BuildContext {
  2. T watch<T>() {
  3. return Provider.of<T>(this);
  4. }
  5. }

context.read是ReadContext类中的函数,继承了BuildContext,其实就是封装了一下获取Provider的方法,用read方法获取的Provider,那么当value改变的时候,不会使页面重建,而且这个方法不能再StatelessWidget.build和State.build方法中调用,也就是说可在这些方法外面随意调用。

context.watch是WatchContext类中的函数,刚好和read相反,WatchContext中的watch方法和ReadContext中的read方法是相似的,但是watch方法会导致widget重构。

总结

Flutter的状态管理机制涵盖了短时状态和共享状态,足够满足我们在日常开发中所遇到的数据更新去刷新UI的需求,个人感觉比命令式编程轻松了很多,后面我也会在此基础上看看是否能够封装一套自己的状态管理框架,欢迎同学们一起交流讨论。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/706630
推荐阅读
相关标签
  

闽ICP备14008679号