Flutter 状态管理之Provider

作者: _兜兜转转_ | 来源:发表于2020-08-18 11:36 被阅读0次

    flutter中状态管理是重中之重,每当谈这个话题,总有说不完的话。

    在正式介绍 Provider 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
    如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。

    image
    但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
    image
    这又是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
    Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
    State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
    这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

    Provider 是什么

    通过使用Provider而不用手动编写InhertedWidget,您将获取自动分配、延迟加载、大大减少每次创建新类的代码。

    首先在yaml中添加,具体版本号参考:官方Provider pub,当前版本号是4.1.3.

      Provider: ^4.1.3
    

    然后运行

    flutter pub get
    

    获取到最新的包到本地,在需要的文件夹内导入

    import 'package:provider/provider.dart';
    

    简单例子

    我们还用点击按钮新增数字的例子

    首先创建存储数据的Model

    class ProviderModel extends ChangeNotifier {
    
        int _count=0;
        ProviderModel();
       void plus() {
       /// 在数据变动的时候通知监听者刷新UI
        _count = _count + 1;
        notifyListeners();
      }
    }
    

    构造view

    /// 使用Consumer来监听全局刷新UI
    Consumer<ProviderModel>(
            builder:
                (BuildContext context, ProviderModel value, Widget child) {
              print('Consumer 0 刷新');
              _string += 'c0 ';
              return _Row(
                value: value._count.toString(),
                callback: () {
                  context.read<ProviderModel>().plus();
                },
              );
            },
            child: _Row(
              value: '0',
              callback: () {
                context.read<ProviderModel>().plus();
              },
            ),
          )
    

    测试下看下效果:

    image

    单个Model多个小部件分别刷新(局部刷新)

    单个model实现单个页面多个小部件分别刷新,是使用Selector<Model,int>来实现,首先看下构造函数:

    class Selector<A, S> extends Selector0<S> {
      /// {@macro provider.selector}
      Selector({
        Key key,
        @required ValueWidgetBuilder<S> builder,
        @required S Function(BuildContext, A) selector,
        ShouldRebuild<S> shouldRebuild,
        Widget child,
      })  : assert(selector != null),
            super(
              key: key,
              shouldRebuild: shouldRebuild,
              builder: builder,
              selector: (context) => selector(context, Provider.of(context)),
              child: child,
            );
    }
    

    可以看到Selector继承了Selector0,再看Selector关键build代码:

    class _Selector0State<T> extends SingleChildState<Selector0<T>> {
      T value;
      Widget cache;
      Widget oldWidget;
    
      @override
      Widget buildWithChild(BuildContext context, Widget child) {
        final selected = widget.selector(context);
    
        var shouldInvalidateCache = oldWidget != widget ||
            (widget._shouldRebuild != null &&
                widget._shouldRebuild.call(value, selected)) ||
            (widget._shouldRebuild == null &&
                !const DeepCollectionEquality().equals(value, selected));
        if (shouldInvalidateCache) {
          value = selected;
          oldWidget = widget;
          cache = widget.builder(
            context,
            selected,
            child,
          );
        }
        return cache;
      }
    }
    

    根据我们传入的_shouldRebuild来判断是否需要更新,如果需要更新则执行widget.build(context,selected,child),否则返回已经缓存的cache.当没有_shouldRebuild参数时则根据widget.selector(ctx)的返回值判断是否和旧值相等,不等则更新UI

    所以我们不写shouldRebuild也是可以的。

    局部刷新用法

      Widget build(BuildContext context) {
        print('page 1');
        _string += 'page ';
        return Scaffold(
          appBar: AppBar(
            title: Text('Provider 全局与局部刷新'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Text('全局刷新<Consumer>'),
                Consumer<ProviderModel>(
                  builder:
                      (BuildContext context, ProviderModel value, Widget child) {
                    print('Consumer 0 刷新');
                    _string += 'c0 ';
                    return _Row(
                      value: value._count.toString(),
                      callback: () {
                        context.read<ProviderModel>().plus();
                      },
                    );
                  },
                  child: _Row(
                    value: '0',
                    callback: () {
                      context.read<ProviderModel>().plus();
                    },
                  ),
                ),
                SizedBox(
                  height: 40,
                ),
                Text('局部刷新<Selector>'),
                Selector<ProviderModel, int>(
                  builder: (ctx, value, child) {
                    print('Selector 1 刷新');
                    _string += 's1 ';
                    return Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text('Selector<Model,int>次数:' + value.toString()),
                        OutlineButton(
                          onPressed: () {
                            context.read<ProviderModel>().plus2();
                          },
                          child: Icon(Icons.add),
                        )
                      ],
                    );
                  },
                  selector: (ctx, model) => model._count2,
                  shouldRebuild: (m1, m2) {
                    print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '数据相等,本次不刷新'}');
                    return m1 != m2;
                  },
                ),
                SizedBox(
                  height: 40,
                ),
                Text('局部刷新<Selector>'),
                Selector<ProviderModel, int>(
                  selector: (context, model) => model._count3,
                  shouldRebuild: (m1, m2) {
                    print('s2:$m1 $m2 ${m1 != m2 ? '不相等,本次刷新' : '数据相等,本次不刷新'}');
                    return m1 != m2;
                  },
                  builder: (ctx, value, child) {
                    print('selector 2 刷新');
                    _string += 's2 ';
                    return Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text('Selector<Model,int>次数:' + value.toString()),
                        OutlineButton(
                          onPressed: () {
                            ctx.read<ProviderModel>().plus3();
                          },
                          child: Icon(Icons.add),
                        )
                      ],
                    );
                  },
                ),
                SizedBox(
                  height: 40,
                ),
                Text('刷新次数和顺序:↓'),
                Text(_string),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    OutlineButton(
                      child: Icon(Icons.refresh),
                      onPressed: () {
                        setState(() {
                          _string += '\n';
                        });
                      },
                    ),
                    OutlineButton(
                      child: Icon(Icons.close),
                      onPressed: () {
                        setState(() {
                          _string = '';
                        });
                      },
                    )
                  ],
                )
              ],
            ),
          ),
        );
      }
    
    

    效果:

    image

    当我们点击局部刷新s1,执行s1builds1不相等,s2相等不刷新。输出:

    flutter: s2:5 5 数据相等,本次不刷新
    flutter: s1:6 7 不相等,本次刷新
    flutter: Selector 1 刷新
    flutter: Consumer 0 刷新
    

    当点击s2,s2的值不相等刷新UI,s1数据相等,不刷新UI.

    flutter: s2:2 3 不相等,本次刷新
    flutter: selector 2 刷新
    flutter: s1:0 0 数据相等,本次不刷新
    flutter: Consumer 0 刷新
    

    可以看到上边2次Consumer每次都刷新了,我们探究下原因。

    Consumer 全局刷新

    Consumer继承了SingleCHildStatelessWidget,当我们在ViewModel中调用notification则当前widget被标记为dirty,然后在build中执行传入的builder函数,在下帧则会刷新UI

    Selector<T,S>则被标记dirty时执行_Selector0State中的buildWithChild(ctx,child)函数时,根据selected_shouldRebuild来判断是否需要执行widget.builder(ctx,selected,child)(刷新UI).

    其他用法

    多model写法

    只需要在所有需要model的上级包裹即可,当我们一个page需要2model的时候,我么通常这样子写:

    class BaseProviderRoute extends StatelessWidget {
      BaseProviderRoute({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MultiProvider(
          providers: [
            ChangeNotifierProvider<ProviderModel>(
              create: (_) => ProviderModel(),
            ),
            ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()),
          ],
          child: BaseProvider(),
        );
      }
    }
    

    当然是用的时候和单一model一致的。

      Selector<ProviderModel2, int>(
        selector: (context, model) => model.value,
        builder: (ctx, value, child) {
          print('model2 s1 刷新');
          _string += 'm2s1 ';
          return Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Selector<Model2,int>次数:' + value.toString()),
              OutlineButton(
                onPressed: () {
                  ctx.read<ProviderModel2>().add(2);
                },
                child: Icon(Icons.add),
              )
            ],
          );
        },
      ),
    
    

    watch && read

    watch源码是Provider.of<T>(this),默认Provider.of<T>(this)listen=true.

    static T of<T>(BuildContext context, {bool listen = true}){
    final inheritedElement = _inheritedElementOf<T>(context);
    
        if (listen) {
          context.dependOnInheritedElement(inheritedElement);
        }
    
        return inheritedElement.value;
    }
    

    read源码是Provider.of<T>(this, listen: false),watch/read只是写法简单一点,并无高深结构。

    当我们想要监听值的变化则是用watch,当想调用model的函数时则使用read

    参考

    相关文章

      网友评论

        本文标题:Flutter 状态管理之Provider

        本文链接:https://www.haomeiwen.com/subject/bebhjktx.html