Provider

从名字就可以看出,Provider 是一个用来提供数据的框架。它是 InheritedWidget 的语法糖,提供了依赖注入的功能,允许在 Widget 树中更加灵活地处理和传递数据。

为了使用 Provider,我们需要解决以下 3 个问题:

  • 资源(即数据状态)如何封装?
  • 资源放在哪儿,才都能访问得到?
  • 具体使用时,如何取出资源?
  1. 在使用 Provider 之前,我们首先需要在 pubspec.yaml 文件中添加 Provider 的依赖:
  1. dependencies:
  2. flutter:
  3. sdk: flutter
  4. provider: 3.0.0+1 #provider依赖
  1. 由于第二个页面还需要修改状态,因此我们还需要在数据状态的封装上包含更改数据的方法:
  1. //定义需要共享的数据模型,通过混入ChangeNotifier管理听众
  2. class CounterModel with ChangeNotifier {
  3. int _count = 0;
  4. //读方法
  5. int get counter => _count;
  6. //写方法
  7. void increment() {
  8. _count++;
  9. notifyListeners();//通知听众刷新
  10. }
  11. }

可以看到,我们在资源封装类中使用 mixin 混入了 ChangeNotifier。这个类能够帮助我们管理所有依赖资源封装类的听众。当资源封装类调用 notifyListeners 时,它会通知所有听众进行刷新。

  1. 资源已经封装完毕,接下来我们就需要考虑把它放到哪儿了。

因为 Provider 实际上是 InheritedWidget 的语法糖,所以通过 Provider 传递的数据从数据流动方向来看,是由父到子(或者反过来)。这时我们就明白了,原来需要把资源放到 FirstPage 和 SecondPage 的父 Widget,也就是应用程序的实例 MyApp 中(当然,把资源放到更高的层级也是可以的,比如放到 main 函数中):

  1. class MyApp extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. //通过Provider组件封装数据资源
  5. return ChangeNotifierProvider.value(
  6. value: CounterModel(),//需要共享的数据资源
  7. child: MaterialApp(
  8. home: FirstPage(),
  9. )
  10. );
  11. }
  12. }

这里需要注意的是,由于封装的数据资源不仅需要为子 Widget 提供读的能力,还要提供写的能力,因此我们需要使用 Provider 的升级版 ChangeNotifierProvider。而如果只需要为子 Widget 提供读能力,直接使用 Provider 即可。

  1. 最后,在注入数据资源完成之后,我们就可以在 FirstPage 和 SecondPage 这两个子 Widget 完成数据的读写操作了。

关于读数据,与 InheritedWidget 一样,我们可以通过 Provider.of 方法来获取资源数据。而如果我们想写数据,则需要通过获取到的资源数据,调用其暴露的更新数据方法(本例中对应的是 increment),代码如下所示:

  1. //第一个页面,负责读数据
  2. class FirstPage extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. //取出资源
  6. final _counter = Provider.of<CounterModel>(context);
  7. return Scaffold(
  8. //展示资源中的数据
  9. body: Text('Counter: ${_counter.counter}'),
  10. //跳转到SecondPage
  11. floatingActionButton: FloatingActionButton(
  12. onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()))
  13. ));
  14. }
  15. }
  16. //第二个页面,负责读写数据
  17. class SecondPage extends StatelessWidget {
  18. @override
  19. Widget build(BuildContext context) {
  20. //取出资源
  21. final _counter = Provider.of<CounterModel>(context);
  22. return Scaffold(
  23. //展示资源中的数据
  24. body: Text('Counter: ${_counter.counter}'),
  25. //用资源更新方法来设置按钮点击回调
  26. floatingActionButton:FloatingActionButton(
  27. onPressed: _counter.increment,
  28. child: Icon(Icons.add),
  29. ));
  30. }
  31. }

Consumer

滥用 Provider.of 方法也有副作用,那就是当数据更新时,页面中其他的子 Widget 也会跟着一起刷新。

Provider 可以精确地控制 UI 刷新粒度,而这一切是基于 Consumer 实现的。Consumer 使用了 Builder 模式创建 UI,收到更新通知就会通过 builder 重新构建 Widget。

在下面的例子中,我们在 SecondPage 中去掉了 Provider.of 方法来获取 counter 的语句,在其真正需要这个数据资源的两个子 Widget,即 Text 和 FloatingActionButton 中,使用 Consumer 来对它们进行了一层包装:

  1. class SecondPage extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. //使用Consumer来封装counter的读取
  6. body: Consumer<CounterModel>(
  7. //builder函数可以直接获取到counter参数
  8. builder: (context, CounterModel counter, _) => Text('Value: ${counter.counter}')),
  9. //使用Consumer来封装increment的读取
  10. floatingActionButton: Consumer<CounterModel>(
  11. //builder函数可以直接获取到increment参数
  12. builder: (context, CounterModel counter, child) => FloatingActionButton(
  13. onPressed: counter.increment,
  14. child: child,
  15. ),
  16. child: TestIcon(),
  17. ),
  18. );
  19. }
  20. }

多状态的资源封装

  1. 首先,我们来看看如何封装。

多个数据状态与单个数据的封装并无不同,如果需要支持数据的读写,我们需要一个接一个地为每一个数据状态都封装一个单独的资源封装类;而如果数据是只读的,则可以直接传入原始的数据对象,从而省去资源封装的过程。

  1. 接下来,我们再看看如何实现注入。

如果我们想注入多个资源,则可以使用 Provider 的另一个升级版 MultiProvider,来实现多个 Provider 的组合注入。

  1. class MyApp extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return MultiProvider(providers: [
  5. Provider.value(value: 30.0),//注入字体大小
  6. ChangeNotifierProvider.value(value: CounterModel())//注入计数器实例
  7. ],
  8. child: MaterialApp(
  9. home: FirstPage(),
  10. ));
  11. }
  12. }
  1. 最后我们来看看如何获取这些资源
  1. final _counter = Provider.of<CounterModel>(context);//获取计时器实例
  2. final textSize = Provider.of<double>(context);//获取字体大小

如果以 Consumer 的方式来获取资源的话,我们只要使用 Consumer2 对象(这个对象提供了读取两个数据资源的能力)

  1. //使用Consumer2获取两个数据资源
  2. Consumer2<CounterModel,double>(
  3. //builder函数以参数的形式提供了数据资源
  4. builder: (context, CounterModel counter, double textSize, _) => Text(
  5. 'Value: ${counter.counter}',
  6. style: TextStyle(fontSize: textSize))
  7. )