状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。
本文我们首先了解下什么是状态和状态管理呢?然后我们来了解官方的状态管理库Provider的使用,最后分析下Provider背后的秘密。
Provider状态管理
状态
Flutter是声明式编程,Widget定义的UI都是在build()
函数中实现的,这个函数的功能就是将状态转换成UI。
UI = f(state)
官方对状态的定义如下:
whatever data you need in order to rebuild your UI at any moment in time
翻译过来就是:状态就是任何时间任何场景下重构UI所需要的数据。
这里面至少可以看到两层含义:
- 状态就是数据;
- 状态的改变驱动了UI的改变。
状态的分类
我们可以把状态分为局部状态和全局状态。
局部状态就是Widget中内部持有的状态,典型代表就是StatefuleWidget和它对应的State。局部状态只会影响单个Widget的UI呈现。
当某个状态需要在多个Widget使用,或者在整个APP中使用,那它就是全局状态了。全局状态的典型代表就是InheritedWidget。
我们在InheritedWidget的使用和源码分析这篇文章中已经详细介绍过了InheritedWidget的相关内容,当然我们也提到过它的一些不是太完善的地方。
状态管理库
我们这里所说的状态管理库主要是指对全局状态的一些处理库,除了InheritedWidget外,还有一些最近非常流行的库:
它目前是评分最高的库,适合大型的项目。但是它有一个缺点就是理解起来比较困难,编写代码方式也很独特,需要编写一些重复的代码模板。
它是Flutter官方团队共同维护的一个项目,由于有官方背景,所以不用担心后期的维护升级问题。
getx是目前上升趋势最快的一个库,使用非常简单,代码也很简介,功能很多。
当然还有其他一些库,譬如mobx,flutter_redux等,当然你很大可能也不会用到。
我们将会对Provider和getx这两个库的使用和源码进行介绍。
Provider的使用
和介绍InheritedWidget时使用的案例类似,本文讲解Provider的时候使用是一个简单的计数器案例:有一个number的全局状态,有三个Widget会使用到它,点击FloatingActionButton可以将number的值加1。效果如下:
Demo当然复杂的多界面逻辑的实现方法使用的方法是一样的。譬如实现下面的功能:
官方的示例
基本使用
使用前得先引入库:
dependencies:
provider: ^5.0.0
接下来我们分三步来了解它的使用:
- 将number封装到ChangeNotifier中,创建需要共享的状态
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
ChangeNotifier是Flutter Framework的基础类,不是Provider库中的类。ChangeNotifier继承自Listenable,也就是ChangeNotifier可以通知观察者值的改变(实现了观察者模式)。
NumberModel有一个_number
状态,然后提供了获取的方法get
和设置set
的方法。
- 在应用程序的顶层添加ChangeNotifierProvider
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
child: MyApp(),
),
);
}
将应用的顶层设置为ChangeNotifierProvider, 然后将MyApp()变为它的子Widget。
ChangeNotifierProvider的create
函数需要返回ChangeNotifier。
- 其它Widget使用共享的状态
有四个地方需要使用到共享的状态,三个显示文字的Text Widget和FloatingActionButton。
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取NumberModel的number
int number = Provider.of<NumberModel>(context).number;
return Container(
child: Text(
"点击次数: $number",
style: TextStyle(fontSize: 30),
),
);
}
}
我们将Text Widget封装成了NumberWidget1, 通过int number = Provider.of<NumberModel>(context).number;
获取到NumberModel的number
值,然后就可以显示了。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1 获取NumberModel
NumberModel model = Provider.of<NumberModel>(context);
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 2 修改number值
model.number++;
},
child: Icon(Icons.add),
));
}
}
FloatingActionButton也需要通过Provider.of<NumberModel>(context)
方法先拿到NumberModel,然后调用set
方法改变number
的值。
全部代码:
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
NumberModel model = Provider.of<NumberModel>(context);
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
model.number++;
},
child: Icon(Icons.add),
));
}
}
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
int number = Provider.of<NumberModel>(context).number;
return Container(
child: Text(
"点击次数: $number",
style: TextStyle(fontSize: 30),
),
);
}
}
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
Consumer
问题:
Provider.of
有一个问题,就是当状态值发生变化后,Provider.of
所在的Widget整个buil
d方法都会重新构建。
上面的例子中,FloatingActionButton会引起Scaffold的重构,所以对性能的影响是最大的。
Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: Icon(Icons.add),
);
},
)
我们将FloatingActionButton用Consumer包裹,builder
中的value
参数就是我们需要的NumberModel了。
这里我们可以进一步优化一下,对child进行复用。
Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
我们将child传入Consumer的构造函数就能实现复用了。
child复用的逻辑我们在前一篇关于动画源码的文章中有解释,如果需要可以回头参阅。
差异部分的代码如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: Consumer<NumberModel>(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
}
}
Consumer
问题:
Consumer
总归还是需要重构的,其实我们使用FloatingActionButton的时候只是用到了NumberModel的设置方法,根本没有用到它的_number
属性,所以即使_number
改变了,我们也是可以不需要重构的。
如果不需要重构,我们可以使用Selector:
Selector<NumberModel, NumberModel>(
selector: (ctx, numberModel) => numberModel,
shouldRebuild: (previous, next) => false,
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
)
代码解释:
- Selector的泛型中有两个参数类型,第一个是原始类型,第二个是转换后的类型,也就是说Selector多了一个对数据进行转换的功能;
-
selector
是进行数据类型转换的函数; -
shouldRebuild
是确实是否需要重构,我们明显是不需要的,所以传false; -
builder
和Consumer的功能就是类似的了。
差异部分的代码如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
),
floatingActionButton: Selector<NumberModel, NumberModel>(
selector: (ctx, numberModel) => numberModel,
shouldRebuild: (previous, next) => false,
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
}
}
多个状态的使用
有时候某个Widget可能需要使用多个状态,我们接下来就介绍这种情况的使用方法。
- 创建多个需要共享的状态
class RandomNumberModel extends ChangeNotifier {
int _randomNumber = Random().nextInt(100);
int get randomNumber => _randomNumber;
void resetRandomNumber() {
_randomNumber = Random().nextInt(100);
notifyListeners();
}
}
我们再创建一个RandomNumberModel,里面有一个随机的数值_randomNumber
, 并且设置获取方法get
和设置方法resetRandomNumber
。
- 将应用程序的顶层改为MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<NumberModel>(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider<RandomNumberModel>(
create: (ctx) => RandomNumberModel(),
),
],
child: MyApp(),
),
);
}
MultiProvider的providers
放置的是共享的多个Provider。
- 其它Widget使用共享的状态
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 读取
int number = Provider.of<NumberModel>(context).number;
// 读取
int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
return Container(
// 使用
child: Text(
"点击次数: $number 随机数: $randomNumber",
style: TextStyle(fontSize: 30),
),
);
}
}
我们可以通过Provider.of
分别取到NumberModel和RandomNumberModel,然后读取到相应的值。
Consumer2
class NumberWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer2<NumberModel, RandomNumberModel>(
builder: (context, value, value2, child) {
return Text("点击次数: ${value.number} 随机数: ${value2.randomNumber}",
style: TextStyle(fontSize: 30));
},
),
);
}
}
Consumer2中两个泛型代表使用的哪两个数据,build
方法中的value
就是NumberModel,value2
就是RandomNumberModel,然后读取到相应的值。
Selector2
class NumberWidget3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
builder: (context, value, child) {
return Text("点击次数: ${value.item1} 随机数: ${value.item2}",
style: TextStyle(fontSize: 30));
},
shouldRebuild: (previous, next) => previous != next,
)
);
}
}
-
Selector2有三个泛型参数:NumberModel和RandomNumberModel代表使用的两个数据类型,第三个参数表示由前两个数据转换成的新的数据类型,我们需要使用两个
int
值。
使用Tuple2需要引入三方库
tuple: ^2.0.0
。使用它的优点是它内置了==
比较操作符,不需要我们去自己比较元素是否相等了。
-
selector
的三个参数为:BuildContext,NumberModel和RandomNumberModel, 返回值就是转换后的数据。
builder
方法中就可以直接使用value.item1
和value.item2
了。
-
shouldRebuild
方法的previous
和next
的类型是Tuple2<int, int>,可以直接比较。如果相同就不重构了。
多个状态使用的补充
Consumer2还有几个好兄弟:,Consumer3,Consumer4,Consumer5,Consumer6。
Selector2也有几个好兄弟:,Selector3,Selector4,Selector5,Selector6。
通过名字可以知道,他们分别可以组合对应的多个数据。
全部代码:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<NumberModel>(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider<RandomNumberModel>(
create: (ctx) => RandomNumberModel(),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Provider"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
),
floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
child: Icon(Icons.add),
builder: (context, value, value2, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
value2.resetRandomNumber();
},
child: child,
);
},
),
);
}
}
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
int number = Provider.of<NumberModel>(context).number;
int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
return Container(
child: Text(
"点击次数: $number 随机数: $randomNumber",
style: TextStyle(fontSize: 30),
),
);
}
}
class NumberWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer2<NumberModel, RandomNumberModel>(
builder: (context, value, value2, child) {
return Text("点击次数: ${value.number} 随机数: ${value2.randomNumber}",
style: TextStyle(fontSize: 30));
},
),
);
}
}
class NumberWidget3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
builder: (context, value, child) {
return Text("点击次数: ${value.item1} 随机数: ${value.item2}",
style: TextStyle(fontSize: 30));
},
shouldRebuild: (previous, next) => previous != next,
)
);
}
}
class NumberModel extends ChangeNotifier {
int _number = 0;
int get number => _number;
set number(int value) {
_number = value;
notifyListeners();
}
}
class RandomNumberModel extends ChangeNotifier {
int _randomNumber = Random().nextInt(100);
int get randomNumber => _randomNumber;
void resetRandomNumber() {
_randomNumber = Random().nextInt(100);
notifyListeners();
}
}
Provider源码解析
- Provider的基本架构如下:
- 所有的Provider都继承自InheritedProvider;
-
InheritedProvider持有一个_CreateInheritedProvider对象
_delegate
,_delegate
持有_ValueInheritedProviderState对象,_ValueInheritedProviderState对象通过createState()
方法调用了InheritedProvider的create()
方法生成了_value
,_value
也就是开发者提供的可监测对象ChangeNotifier;
create()
只有在需要使用_value
时候才会调用,并不是InheritedProvider插入Widget Tree时候就调用,属于懒加载的实现。
-
InheritedProvider有一个InheritedWidget的子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的
_value
的值;
也就是说Provider依赖于InheritedWidget,找到对应的InheritedWidget就能获取对应的
_value
的值。
-
Widget重构的时候如果调用
Provider.of
方法,会找到_value
的值并且监听它的变化。
- Provider的局部刷新逻辑如下:
-
_value
值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents
方法,调用依赖Widget的didChangeDependencies
, 这两个方法都会调用markNeedsBuild()
,进行重构; -
Widget重构的时候会调用
Provider.of
方法,更新对_value
的监听,为下次重构做准备。
- Consumer和Selector的优化逻辑:
Consumer和Selector只是封装了一层SingleChildStatefulWidget,重构的范围限定在Consumer和Selector内部,内部调用的还是Provider.of
方法。
- MultiProvider的逻辑:
MultiProvider就是嵌套了多个Provider,其他和单个Provider没有什么差别。
总结
其实Provider库还提供了其他的几个Provider,ListenableProvider
,ValueListenableProvider
,StreamProvider
和FutureProvider
,它们都是我们开发中的可选项。
至此,我们将Provider库的使用方式和底层的逻辑解释完了。
网友评论