FlutterDojo设计之道—状态管理之路(六)

  通过前面这么多文章的学习,Flutter的状态管理之路终于要接近尾声了。ios

  其实前面讲了这么多,最后的结论依然是——Provider真香。这毕竟是官方推荐的状态管理方案,就目前而言,绝大部分的场景均可以使用Provider来进行状态管理,同时也基本上是最佳方案。git

  

Google的风格还真是这样,先不给出任何指定方案,你们百花齐放,最后选一个好一点的改改,这就成了官方方案!

  可是咱们为何还要讲这么多其它的状态管理方案呢?实际上并很少,你们再去翻阅下前面的文章就能够发现,我讲的都是Flutter中的原生方案,关于第三方的Redux、scope_model等方案,其实我也没有涉及,其缘由就是但愿读者可以从根本原理上来了解「什么是状态管理」、「怎么进行状态管理」以及「状态管理各类方案的优缺点」,只有了解了这些,再使用Provider进行状态管理,就不只仅是调用API这么简单了,你会对其根源有所了解,这才是本系列文章的核心所在。github

  Provider是Flutter官方提供的状态管理解决方案,其基本原理是InheritedWidget,Pub地址以下所示。算法

  https://github.com/rrousselGit/providerapp

  引入 less

  Provider的迭代很快,目前最新版本是4.x,在pubspec.yaml中添加Provider的依赖,代码以下所示。ide

  dependencies:
flutter:
sdk: flutter

函数

  # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
provider: ^4.3.2+1



学习

  执行pub get以后,便可更新Provider库。ui

  Provider的核心实际上就是InheritedWidget,它其实是对InheritedWidget的封装,让InheritedWidget在数据管理上可以更加方便的被开发者所使用。

  因此,若是你的InheritedWidget比较熟悉,那么在使用Provider的时候,你必定会有一种似曾相识的感受。

  建立DataModel

  在使用Provider以前,首先须要对Model进行下处理,经过mixin,为Model提供notifyListeners的能力。

  class TestModel with ChangeNotifier {
int modelValue;

  int get value => modelValue;

  TestModel({this.modelValue = 0});

  void add() {
modelValue++;
notifyListeners();
}
}




  在这个Model中,管理了须要共享的数据,同时,提供了修改数据的方法,惟一不同的是,在修改数据后,须要经过ChangeNotifier提供的notifyListeners()来刷新数据。

  ChangeNotifierProvider

  使用ChangeNotifierProvider,维护须要管理的数据,代码以下。

  class ProviderState1Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChildWidget1(),
SizedBox(height: 24),
ChildWidget2(),
],
),
),
);
}
}


















  经过ChangeNotifierProvider的create函数,建立初始化的Model。同时建立其Child,这个风格和InheritedWidget是否是有殊途同归之妙。

  

Provider提供了不少不一样类型的Provider,这里先只用了解ChangeNotifierProvider
管理数据之Provider.of

  经过Provider管理的数据,能够经过Provider.of(context);来读取数据,代码以下所示。

  var style = TextStyle(color: Colors.white);

  class ChildWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidget1 build');
var model = Provider.of(context);
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${model.value}', style: style),
RaisedButton(
onPressed: () => model.add(),
child: Text('add'),
),
],
),
);
}
}





















  还能够经过model来获取操做数据的方法add()。

  效果如图所示。

  


  这样就完成了一个最简单的Provider使用方法。

  可是经过日志能够发现,每次调用Provider.of(context);后,都会致使Context所处的Widget执行Rebuild。

  I/flutter (18490): ChildWidget2 build
I/flutter (18490): ChildWidget1 build

  是否是又似曾相识?是的,这就是前面文章中所提到的dependOnInheritedWidgetOfExactType的问题,它会对调用者进行记录,在数据更新时,对数据进行rebuild操做。

  另外,上面的例子中,实际上还隐藏了一个很容易被初学者忽视的问题,咱们来看下这段代码。

  RaisedButton(
onPressed: () => model.add(),
child: Text('add'),
),



  在button的点击事件中,咱们并无直接使用每次调用Provider.of(context).add(),而是将每次调用Provider.of(context)抽取了出来,为何要画蛇添足呢?

  其实你们能够尝试下这样调用,点击后,会报错,以下所示。

  Tried to listen to a value exposed with provider, from outside of the widget tree.

  This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.

  To fix, write:
Provider.of(context, listen: false);

  It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.

  简单的说,就是在button的event handler中,触发了Provider.of,可是这个时候,传入的Context并不在Widget中,致使notifyListeners出错。

  解决方法有两个,一个就是将Provider.of抽取出来,用Widget的Context来获取Model,另外一个呢,就是经过Provider.of的另外一个参数来去掉监听的注册。

  RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),



  经过listen: false,去掉默认注册的监听。

  

Provider.of的默认实现中,listen = true,至于为何,你们能够看这里的讨论。https://github.com/rrousselGit/provider/issues/188#issuecomment-526259839 https://github.com/rrousselGit/provider/issues/313#issuecomment-576156922

  所以,咱们总结了两条Provider的使用规则。

  Provider.of(context):用于须要根据数据的变化而自动刷新的场景

  Provider.of(context, listen: false):用于只须要触发Model中的操做而不关心刷新的场景

  所以对应的,在新版本的Provider中,做者还提供了两个Context的拓展函数,来进一步简化调用。

  T watch()

  T read()

  他们就分别对应了上面的两个使用场景,因此在上面的示例中,Text获取数据的方式,和在Button中点击的方式还能够写成下面这张形式。

  Text('watch: ${context.watch().value}', style: style)

  RaisedButton(
onPressed: () => context.read().add(),
child: Text('add'),
),



  

代码地址 Flutter Dojo-Backend-ProviderState1Widget
管理数据之Consumer

  获取Provider管理的数据Model,有两种方式,一种是经过Provider.of(context)来获取,另外一种,是经过Consumer来获取,在设计Consumer时,做者给它赋予了两个功能。

  当传入的BuildContext中,不存在指定的Provider时,Consumer容许咱们从Provider中的获取数据(其缘由就是Provider使用的是InheritedWidget,因此只能遍历父Widget,当指定的Context对应的Widget与Provider处于同一个Context时,就没法找到指定的InheritedWidget了)

  提供更加精细的数据刷新范围,避免无谓的刷新

  建立新的Context环境

  首先,咱们来看下第一个功能。

  @override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${Provider.of(context).value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
),
);
}

























  在上面的这个例子中,ChangeNotifierProvider和使用Provider的Widget,使用的是同一个Context,因此确定是没法找到对应的InheritedWidget的,因此会报错。

  The following ProviderNotFoundException was thrown building ProviderState2Widget(dirty):
Error: Could not find the correct Provider above this ProviderState2Widget Widget

  This likely happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:

  - The provider you are trying to read is in a different route.

  Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.

  - You used a `BuildContext` that is an ancestor of the provider you are trying to read.

  Make sure that ProviderState2Widget is under your MultiProvider/Provider.
This usually happen when you are creating a provider and trying to read it immediately.

  解决方法也很简单,一个是将须要使用Provider的Widget抽取出来,放入一个新的Widget中,这样在这个Widget中,就有了属于本身的Context,另外一种,就是经过Consumer,来建立一个新的Context环境,代码以下所示。

  @override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Consumer(
builder: (BuildContext context, value, Widget child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${value.value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
);
},
),
),
),
),
);
}





























控制更加精细的刷新范围

  来看下下面这个例子。

  class ProviderState2Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValue: 1),
child: NewWidget(),
);
}
}







  class NewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Text('Model data: ${Provider.of(context).value}', style: style),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
);
}
}
























  在调用Provider.of的时候,会形成Context范围内的Widget执行Rebuild,这个在前面的例子中,已经看过了。那么要解决这个问题,也很简单,只须要将须要刷新的Widget,用Consumer包裹便可,这样在收到notifyListeners时,就只有Consumer范围内的Widget会进行刷新了,其它范围的地方,就不会被迫刷新了。在Consumer的builder中,能够获取指定泛型的数据对象,代码以下所示。

  @override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Child1', style: style),
Consumer(
builder: (BuildContext context, value, Widget child) {
return Text(
'Model data: ${value.value}',
style: style,
);
},
),
RaisedButton(
onPressed: () => Provider.of(context, listen: false).add(),
child: Text('add'),
),
],
),
),
),
);
}





























代码地址 Flutter Dojo-Backend-ProviderState2Widget

  那么Consumer到底是为何能够实现更加精细的刷新控制呢?其实原理很简单,前面甚至已经提到了,那就是「在调用Provider.of的时候,会形成Context范围内的Widget执行Rebuild」,因此,只须要将调用的范围尽量的缩小,那么执行Rebuild的范围就会越小,看下Consumer的源码。

  


  能够发现,Consumer就是经过一个Builder,来进行了一层封装,最终仍是调用的Provider.of(context), 看了源码以后,相信你们应该能理解Consumer的这两个功能了。

  more Consumer

  Consumer中存在多个类型的变种,它表明着使用多个数据模型的数据获取方式,如图所示。

  


  其实说简单点,就是在一个Consumer的builder中,同时获取多个不一样类型的数据模型,是一种简单的写法,是一种将嵌套的过程打平的过程。源码中只写到Consumer6,即支持同时最多6个数据类型,若是要支持更多,则须要本身实现了。

  管理数据之Selector

  Selector一样是获取数据的一种方式,从理论上来讲,Selector等于Consumer等于Provider.of,可是它们对数据的控制粒度,才是它们之间根本的区别。

  获取数据的方式,从Provider.of,到Consumer,再到Selector,实际上经历了这样一种进化。

  Provider.of:Context内容进行Rebuild

  Consumer:Model内容变化进行Rebuild

  Selector:Model中的指定内容变化进行Rebuild

  能够发现,虽然都是获取数据,可是其控制的精细程度确是递增的。

  下面就经过一个例子,来演示下Selector的使用场景。

  首先,咱们定义一个数据模型,代码以下所示。

  class TestModel with ChangeNotifier {
int modelValueA;
int modelValueB;

  int get valueA => modelValueA;

  int get valueB => modelValueB;

  TestModel({this.modelValueA = 0, this.modelValueB = 0});

  void addA() {
modelValueA++;
notifyListeners();
}


  void addB() {
modelValueB++;
notifyListeners();
}
}




  在这个数据模型中,管理了两个类型的数据,modelValueA和modelValueB。

  下面是展现界面。

  class ProviderState3Widget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TestModel(modelValueA: 1, modelValueB: 1),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChildWidgetA(),
SizedBox(height: 24),
ChildWidgetB(),
],
),
),
);
}
}

















  var style = TextStyle(color: Colors.white);

  class ChildWidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetA build');
var model = Provider.of(context);
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildA', style: style),
Text('Model data: ${model.valueA}', style: style),
RaisedButton(
onPressed: () => model.addA(),
child: Text('add'),
),
],
),
);
}
}




















  class ChildWidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetB build');
var model = Provider.of(context);
return Container(
color: Colors.blueAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildB', style: style),
Text('Model data: ${model.valueB}', style: style),
RaisedButton(
onPressed: () => model.addB(),
child: Text('add'),
),
],
),
);
}
}





















  效果如图所示。

  


  在上面的代码下,不论咱们点击ChildA的Add,仍是ChildB的Add,整个界面都会Rebuild。即便经过Consumer,也没法作到只刷新对应的数据,缘由在于它们的数据模型是同一个,Consumer只能作到数据模型层面上的更新刷新,可是没法针对同一个数据模型中不一样字段的变换而进行更新。

  因此,Consumer解决方案就是须要将这个数据模式拆成两个,ModelA和ModelB,这样使用MultiProvider管理ChangeNotifierProvider(ModleA)和ChangeNotifierProvider(ModelB),再经过Consumer分别管理ModelA和ModelB,这样才能作到互补干扰的刷新。

  那若是数据模型不能拆分呢?这个时候,就可使用Selector了,先来看下Selector的构造函数。

  


  A表明传入的数据源,例如前面的TestModel

  S表明想要监听的A数据源中的的某个属性,好比TestModel的ModelA

  selector的功能,就是从A数据源中筛选出须要监听的数据S,而后将S传递传给builder进行构造

  shouldRebuild用来覆盖默认的对比算法,能够不设置

  对比算法以下所示。

  

从源码能够发现,Selector判断的标准就是新旧数据Model是否「==」,若是是Collection类型,则经过DeepCollectionEquality来进行比较,官方建议使用https://pub.flutter-io.cn/packages/tuple 来进行简化判断

  有了Selector以后,就能够在同一个数据模型中,根据条件,筛选出不一样的刷新条件了,这样就能够避免数据模型中的某个属性变换而引发的整个数据模型刷新了。

  经过Selector,将上面的代码进行下改造。

  class ChildWidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetA build');
return Selector(
selector: (context, value) => value.modelValueA,
builder: (BuildContext context, value, Widget child) {
return Container(
color: Colors.redAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildA', style: style),
Text('Model data: $value', style: style),
RaisedButton(
onPressed: () => context.read().addA(),
child: Text('add'),
),
],
),
);
},
);
}
}

























  这样经过Selector进行一次筛选,就能够避免同一个Model中不一样的数据刷新致使整个Model Rebuild的问题,例如上面的Selector,指定了须要在TestModel中寻找int类型的数据,其过滤条件是TestModel中的modelValueA这样一个int类型的数据,根据ShouldRebuild(默认实现)的判断,返回这个状况下,ChildWidgetA是否须要Rebuild。

  与Provider.of相似,在4.1以后,Provider提供了基于BuildContext的拓展函数来简化Selector的使用,例如上面的代码经过selector拓展函数来实现,代码以下所示。

  class ChildWidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('ChildWidgetB build');
return Container(
color: Colors.blueAccent,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('ChildB', style: style),
Builder(
builder: (BuildContext context) {
return Text(
'Model data: ${context.select((TestModel value) => value.modelValueB)}',
style: style,
);
},
),
RaisedButton(
onPressed: () => context.read().addB(),
child: Text('add'),
),
],
),
);
}
}



























  不过须要注意的是,这里须要经过Builder来建立一个子类的Context,避免当前Context的刷新。

  more Selector

  与Consumer相似,Selector一样也有多种不一样的实现。

  


  其实很简单,就是实现多种不一样的数据类型,在这些数据模型中,找到须要监听的那一种类型,这种状况比较经常使用于多个数据模型中具体共同参数的场景。

  上面就是经过Provider来获取被管理的数据的三种方式:Provider.of,Consumer和Selector,它们的功能彻底一致,区别仅仅在于刷新的控制粒度。

相关文章
相关标签/搜索