背景
2019 Google I/O 大会上重磅消息出了支持 flutter_web 之外,另一个便是弃用之前的状态管理 Provide,转而推荐相似的库 Provider;虽然只有一个字母之差使用方式差别却很大,自此,Provider 代替 Provide 成为官方推荐的状态管理方式之一。
Provider也是借助了InheritedWidget,它允许在小部件树中传递数据,允许我们更加灵活地处理数据类型和数据。
为什么需要状态管理
在进行项目的开发时,往往需要管理不同页面之间的数据共享,在页面功能复杂,状态可能会达到几十个甚至上百个,这时将难以清楚的维护这些数据状态。而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就提供了一种状态管理方式 — StatefulWidget。然而发现它仅适合用于在单个 Widget 内部维护其状态。当需要使用跨组件的状态时,StatefulWidget 将不再是一个好的选择。在StatefulWidget中的 State 属于某一个特定的 Widget,在多个 Widget 之间进行通信的时候,虽然可以使用 callback 解决,但当嵌套足够深的话,很容易增大代码的耦合度。
这时候,就迫切的需要一个架构来帮助理清这些关系,状态管理框架应运而生。
为什么选择Provider
Provider 从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个 app 都有它自己的解决方案,我们可以很方便的管理状态,并在合适的时机释放资源。可以说,Provider 的目标就是来替代StatefulWidget。
如何使用
添加依赖
执行
flutter packages get
在需要使用的页面引入
import 'package:provider/provider.dart'
创建数据共享类
这里的数据共享类实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。
import 'package:flutter/material.dart’;
class WeatherInfo with ChangeNotifier {
String _tempType = “celcius”;
int _temperatureVal = 25;
String get temperatureType => _tempType;
set temperatureType(String value) {
_tempType = value;
notifyListeners();
}
int get temperatureVal => _temperatureVal;
set temperatureVal(int value) {
_temperatureVal = value;
notifyListeners();
}
}
这个类意图很清晰,数据就是一个 int 类型的 _temperatureVal、string 类型的_tempType,下划线代表私有。通过 get value 把 _temperatureVal 、_tempType值暴露出来。并提供 set 方法用于更改数据。
这里使用了 mixin 混入了 ChangeNotifier,这个类能够帮助我们自动管理所有听众。
当调用 notifyListeners() 时,它会通知所有听众进行刷新。
提供数据共享
在 build 方法提供初始化数据
class WeatherInfoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
LogUtil.v("WeatherInfoPage build”);
return ChangeNotifierProvider<WeatherInfo>(
builder: (context) => WeatherInfo(),
child: Scaffold(
appBar: AppBar(
title: Text("Provider demo${Random().nextInt(100)}”),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"我在ChangeNotifierProvider中,看我变不变${Random().nextInt(100)}”),
),
MyHeader(),
MyHeaderListenerFalse(),
MyContent(),
Padding(
padding: EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return WeatherInfoPage2();
}));
},
child: Text("consumer 例子”),
),
),
],
),
),
floatingActionButton: MyFloatingActionButton(),
),
);
}
}
ChangeNotifierProvider不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 notifyListeners)
此处的 <T>范型可省略。但是建议大家还是进行声明,这会使你的应用更加健壮。
ChangeNotifierProvider 绑定数据有两种方式:
ChangeNotifierProvider({Key key, @required ValueBuilder builder, Widget child })
ChangeNotifierProvider.value({Key key, @required T notifier, Widget child })
使用基本差不多,区别在于Provider()提供dispose参数,可以传递一个方法销毁的时候被调用,方便StatelessWidget释放资源,并且通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁;
获取状态
这里有两个子控件 MyHeader,MyContent获取状态,MyFloatingActionButton 来更改状态。
- Provider.of(context)
class MyHeader extends StatelessWidget {
Color decideColor(WeatherInfo info) {
return "celcius" == info.temperatureType ? Colors.green : Colors.deepOrange;
}
@override
Widget build(BuildContext context) {
LogUtil.v("WeatherInfoPage MyHeader build”);
WeatherInfo weatherInfo = Provider.of<WeatherInfo>(context);
return Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"我是类中的temperatureType=${weatherInfo.temperatureType}”,
style: TextStyle(
color: decideColor(weatherInfo),
fontSize: 20,
),
),
);
}
}
获取顶层数据最简单的方法就是 Provider.of<T>(context);这里的范型<T>指定了获取 MyHeader向上寻找最近的储存了 T 的祖先节点的数据。通过这个方法获取了顶层的 WeatherInfo。并在 Text 组件中进行使用。
- Consumer
class MyContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
LogUtil.v("WeatherInfoPage MyContent build”);
return Consumer<WeatherInfo>(
builder: (BuildContext context, WeatherInfo weatherInfo, Widget child) {
return Padding(
padding: EdgeInsets.all(8.0),
child: Text("我是类中的temperatureVal=${weatherInfo.temperatureVal}”),
);
},
);
}
}
这里获取顶层数据使用Consumer 获取数据。这里的范型 <T>指定了获取 MyContent向上寻找最近的储存了 T 的祖先节点的数据。
Consumer 使用了 Builder模式,收到更新通知就会通过 builder 重新构建。Consumer<T>代表了它要获取哪一个祖先中的 Model。
Consumer 的 builder 实际上就是一个 Function,它接收三个参数 (BuildContext context, T model, Widget child)。
- context: context 就是 build 方法传进来的 BuildContext 。
- T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
- child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。
然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
这里讲的 Consumer 获取的只有一个范型<T>数据,如果获取两个数据,则可以使用Consumer2<A,B>。使用方式基本上和 Consumer<T>一致,只不过范型改为了两个,并且 builder 方法也变成了 Function(BuildContext context, A value, B value2, Widget child)。
从源码里面可以看到,作者只为提供到了 Consumer6。如果还要要求更多就只有合并数据源了。
- 两者区别
先看下 Consumer 内部实现
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<T>(context),
child,
);
}
可以发现,Consumer就是通过Provider.of<T>(context)来实现的。但是从实现来讲Provider.of<T>(context)比 Consumer简单好用太多,为什么要搞得那么复杂呢?
实际上 Consumer非常有用,它的经典之处在于能够在复杂项目中,极大地缩小你的控件刷新范围。Provider.of<T>(context)将会把调用了该方法的context作为听众,并在notifyListeners的时候通知其刷新。
举个例子来说,我们在 MyHeader使用了Provider.of<T>(context)来获取数据,MyContent 则没有。
- 在 MyHeader 中的 build 方法中添加一个LogUtil.v("WeatherInfoPage MyHeader build”);
- 然后在 MyContent 中的 build 方法中添加一个 LogUtil.v("WeatherInfoPage MyContent build”);
- 点击MyFloatingActionButton浮动按钮,那么会在控制台看到这句输出WeatherInfoPage MyHeader build。
这证明了 Provider.of<T>(context)会导致调用的 context 页面范围的刷新。
那么MyContent页面刷新没有呢? 刷新了,但是只刷新了 Consumer的部分。
假如在应用的页面级别的Widget中,使用了Provider.of<T>(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,都会重新刷新整个页面。虽然有Flutter的自动优化算法给你撑腰,但肯定无法获得最好的性能。
所以在这里建议各位尽量使用 Consumer而不是 Provider.of<T>(context)获取顶层数据。
多个 Provider
从源码中可以看出,作者提供了 MultiProvider 供提供多种状态使用。
MultiProvider(
providers:[
AProvider,
BProvider,
CProvider,
],
child: child,
)
MultiProvider 实际上就是通过每一个 provider 都实现了的 cloneWithChild 方法,用循环把自己一层一层包裹起来。
@override
Widget build(BuildContext context) {
var tree = child;
for (final provider in providers.reversed) {
tree = provider.cloneWithChild(tree);
}
return tree;
}
等价于
AProvider(
child: BProvider(
child: CProvider(
child: child,
),
),
)
总结
使用Provider.of(context),简单易用,但是要数据发生变化时,会进行页面级别rebuild,相当于statefulWidget,此方法将从BuildContext关联的小部件树中查找,它将返回找到的最近的类型变量T
Provider.of<T>( BuildContext context,
{bool listen = true}//listen:默认true监听状态变化,false为不监听状态改变
)也可以使用Consumer组件获取,Consumer可用在没有context的地方,还可以优化性能
Consumer<T>({
@required this.builder,//这边写布局
this.child,//可以控制刷新性能优化,当数据数据发生改变,不会重新build,
})
- 状态管理包裹在MaterialApp()外面作用域是全局,其他作用域在本页面或本页的子Widget中
- MultiProvider()可以提供多个状态
- ChangeNotifierProvider.value()与ChangeNotifierProvider()区别是ChangeNotifierProvider()通过构造器绑定数据并进行监听,当从 Widget Tree 中删除时, 会自动调用dispose 进行销毁释放资源,在需要使用多个状态值时可以使用ChangeNotifierProvider
- 无论使用那种 .value 方式,均建议在 dispose 中进行 listener 的关闭;
@override
void dispose() {
super.dispose();
_homepageNotifier.dispose();
}
最后
如果在使用过程遇到问题,欢迎下方留言交流。