Flutter 状态管理之Provider
在flutter
中状态管理是重中之重,每当谈这个话题,总有说不完的话。
在正式介绍 Provider
为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter
作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。
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
,执行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