Flutter状态管理

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

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

通过setState更新

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

从上述代码可以看出,如果我们需要改变某个值,只需要使用 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也能同步到最新的数据。


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

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


Widget build(BuildContext context) {
  var model = ProviderModel(context);
  return Continer(
    // ···
  );
}

在上面的例子中,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),像下面这样:

class ProviderModel 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 的时候可以调用该方法。而剩下的代码就是 ProviderModel 和它本身的业务逻辑。可以这么理解,调用了 notifyListeners()之后,会执行创建Provider的Build方法重新构建UI

如果使用创建的Provider:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ChangeNotifierProvider(
          create: (context) => ProviderModel(), child: MyHomePage(title: 'Flutter Demo Home Page')),
    );
  }
}

或者

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: ChangeNotifierProvider(
            create: (context) => ProviderModel(),
            child: Consumer<ProviderModel>(builder: (context, viewModel, child) {
              return MyHomePage(title: 'Flutter Demo Home Page');
            })));
  }
}

现在 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 获得该实例。

return Consumer<ProviderModel>(
  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(),
);

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

return Consumer<ProviderModel>(
  builder: (context, cart, child) {
    return AWidget(
      // ...
      child: BWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

换成:

return AWidget(
      // ...
      child: Consumer<ProviderModel>(
      builder: (context, cart, child) {
        return BWidget(
            // ...
            child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

有的时候你不需要模型中的 数据 来改变 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的区别

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

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

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

总结

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值