状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。
本文我们首先了解下什么是状态和状态管理呢?然后我们来了解官方的状态管理库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这两个库的使用和源码进行介绍。
和介绍InheritedWidget时使用的案例类似,本文讲解Provider的时候使用是一个简单的计数器案例:有一个number的全局状态,有三个Widget会使用到它,点击FloatingActionButton可以将number的值加1。效果如下:
当然复杂的多界面逻辑的实现方法使用的方法是一样的。譬如实现下面的功能:
使用前得先引入库:
dependencies:
provider: ^5.0.0
接下来我们分三步来了解它的使用:
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
的方法。
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
child: MyApp(),
),
);
}
将应用的顶层设置为ChangeNotifierProvider, 然后将MyApp()变为它的子Widget。
ChangeNotifierProvider的create
函数需要返回ChangeNotifier。
有四个地方需要使用到共享的状态,三个显示文字的Text Widget和FloatingActionButton。
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取NumberModel的number
int number = Provider.of(context).number;
return Container(
child: Text(
"点击次数: $number",
style: TextStyle(fontSize: 30),
),
);
}
}
我们将Text Widget封装成了NumberWidget1, 通过int number = Provider.of(context).number;
获取到NumberModel的number
值,然后就可以显示了。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1 获取NumberModel
NumberModel model = Provider.of(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(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(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(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(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: Icon(Icons.add),
);
},
)
我们将FloatingActionButton用Consumer包裹,builder
中的value
参数就是我们需要的NumberModel了。
这里我们可以进一步优化一下,对child进行复用。
Consumer(
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(
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
));
}
}
Consumer
问题:
Consumer
总归还是需要重构的,其实我们使用FloatingActionButton的时候只是用到了NumberModel的设置方法,根本没有用到它的_number
属性,所以即使_number
改变了,我们也是可以不需要重构的。
如果不需要重构,我们可以使用Selector:
Selector(
selector: (ctx, numberModel) => numberModel,
shouldRebuild: (previous, next) => false,
builder: (context, value, child) {
return FloatingActionButton(
onPressed: () {
value.number++;
},
child: child,
);
},
child: Icon(Icons.add),
)
代码解释:
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(
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
。
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider(
create: (ctx) => RandomNumberModel(),
),
],
child: MyApp(),
),
);
}
MultiProvider的providers
放置的是共享的多个Provider。
Provider.of
class NumberWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 读取
int number = Provider.of(context).number;
// 读取
int randomNumber = Provider.of(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(
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>(
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,
)
);
}
}
int
值。使用Tuple2需要引入三方库
tuple: ^2.0.0
。使用它的优点是它内置了==
比较操作符,不需要我们去自己比较元素是否相等了。
selector
的三个参数为:BuildContext,NumberModel和RandomNumberModel, 返回值就是转换后的数据。
builder
方法中就可以直接使用value.item1
和value.item2
了。
shouldRebuild
方法的previous
和next
的类型是Tuple2,可以直接比较。如果相同就不重构了。Consumer2还有几个好兄弟:,Consumer3,Consumer4,Consumer5,Consumer6。
Selector2也有几个好兄弟:,Selector3,Selector4,Selector5,Selector6。
通过名字可以知道,他们分别可以组合对应的多个数据。
全部代码:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => NumberModel(),
),
ChangeNotifierProvider(
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(
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(context).number;
int randomNumber = Provider.of(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(
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>(
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();
}
}
_delegate
, _delegate
持有_ValueInheritedProviderState对象,_ValueInheritedProviderState对象通过createState()
方法调用了InheritedProvider的 create()
方法生成了_value
,_value
也就是开发者提供的可监测对象ChangeNotifier;
create()
只有在需要使用_value
时候才会调用,并不是InheritedProvider插入Widget Tree时候就调用,属于懒加载的实现。
_value
的值;也就是说Provider依赖于InheritedWidget,找到对应的InheritedWidget就能获取对应的
_value
的值。
Provider.of
方法,会找到_value
的值并且监听它的变化。_value
值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents
方法,调用依赖Widget的didChangeDependencies
, 这两个方法都会调用markNeedsBuild()
,进行重构;Provider.of
方法,更新对_value
的监听,为下次重构做准备。Consumer和Selector只是封装了一层SingleChildStatefulWidget,重构的范围限定在Consumer和Selector内部,内部调用的还是Provider.of
方法。
MultiProvider就是嵌套了多个Provider,其他和单个Provider没有什么差别。
其实Provider库还提供了其他的几个Provider,ListenableProvider
,ValueListenableProvider
,StreamProvider
和FutureProvider
,它们都是我们开发中的可选项。
至此,我们将Provider库的使用方式和底层的逻辑解释完了。