Flutter 状态管理之Provider
在flutter中状态管理是重中之重,每当谈这个话题,总有说不完的话。
在正式介绍 Provider 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
这又是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。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(); }, ), )
测试下看下效果:
单个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 = ''; }); }, ) ], ) ], ), ), ); }
效果:
当我们点击局部刷新s1,执行s1的build,s1不相等,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需要2个model的时候,我么通常这样子写:
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
参考
- 代码仓库: https://github.com/ifgyong/flutter-example
- 官方Provider: https://github.com/rrousselGit/provider
浙公网安备 33010602011771号