flutter-provider使用
前言
前面简单讲了 InheritedWidget
,其为我们解决了基础数据共享问题,但跨页面状态管理很不方便,比较适合静态数据, 因此 provider
应运而生
provider
其基于 InheritedWidget
,数据共享问题肯定ok,与此同时,还方便了我们跨多个页面进行数据交互,使用灵活,且对于 build 调用比较频繁的 Widget,其也有优化措施,能够减少调用,可以说非常优秀,并且其基于 InheritedWidget
,因此其也是最接近 flutter 风格
的一款跨组件的状态解决方案了
ps
: 顺便提一下,getx
一把嗦一时爽,一直嗦一直爽,同时其也面对着另一个问题,开发风格已经脱离了原本的 flutter 风格
,初级开发很容易对其过度依赖
,这样对 flutter 的理解
,以及后续组件生态
会存在一个断层
,毕竟很多开发者组件是要减少依赖
的,不会基于 getx
进行开发,因此 provider
亦是一个不错的持续解决方案(这我两个都看了并尝试,让我感觉更接近flutter的就是他了,因此写文章以及后续使用也会是他,还有另外几个个人感觉已经被过渡过去了,可以舍弃)
demo地址(provider文件夹):里面也有 inheritedWidget
的案例,可以运行看看效果
本篇文章参考自 provider文档
,仅仅从使用者的角度,去看看怎么使用的(不讨论冗余的源码原理),看了这篇文章,基本上保证能在应用中灵活解决各种问题了
provider
provider
作为一个跨组件的状态解决方案,下面会讲解,使用它是怎么解决我们的组件状态的
ps
:后面看到的参数 _、__
之类的下划线,其实他们也是参数名字,只不过用不到所以就这么命名了,也可以使用原来的名字,
ChangeNotifier
ChangeNotifier
我们传递数据时需要用到的一个类,我们需要继承他,在里面定义我们的数据,然后通知给其他组件我们的更改便可以了(provider
会自动监听更新,并通知需要更新的地方)
class UserInfo {
String? username;
String? sex;
int? age;
UserInfo();
}
//定义我们的对象,继承自 ChangeNotifier
//一般一个通知定义一个展开的对象(例如:仅代表用户,里面是展开的用户信息)
//实际根据情况在里面可以定义多个同类别对象,这样可以避免过多的通知类和provider出现,且通知代码少了😂
class GlobalNotifier extends ChangeNotifier {
//我们的userInfo可能默认不存在,我们可以在恰当的地方给赋值,这里直接赋值,方便后面直接更改
UserInfo? _userInfo = UserInfo();
//留给外部访问,由于 set 操作要进行通知,就添加新的 set 和 get 方便统一调度
UserInfo? get user => _userInfo;
set user(UserInfo? newValue) {
_userInfo = newValue;
notifyListeners();
}
}
根目录加入全局 Provider
我们在 MaterialApp
添加一个全局的 provider
,绑定我们的 GlobalNotifier
即可,一个ChangeNotifier
需要一个Provider
,多个ChangeNotifier
需要多个Provider
(后面会简单介绍)
Provider
为单纯的只读 provider,无法收到状态更新,只能作为静态使用,使用的比较少,乱用会报错(不推荐,除非就存放静态数据)
ChangeNotifierProvider
是支持通知(支持读写)的 provider
,不仅可以读取内容,还可以接收到更新的状态信息(推荐使用)
//在根目录的我们加入全局Provider就是用这个就行了,不使用 ChangeNotifierProvider.value
//因为根目录也不方便更改,后面看了案例就明白了为什么了
ChangeNotifierProvider(
create: (_) {
return GlobalNotifier();
},
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
),
);
获取 provider 状态原始方法(不推荐)
模仿 InheritedWidget
的读写方式,完美显示和更新,如下所示,但不推荐这么使用
@override
Widget build(BuildContext context) {
//最原始的读取参数的方法,不推荐这么用
final global = Provider.of<GlobalNotifier>(context);
return Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("测试内容1:${global.user?.username}"),
TextButton(
onPressed: () {
final user = global.user;
if (user != null) {
user.username = "test1";
}
global.user = user;
},
child: const Text("更新"),
),
],
),
);
}
获取 provider 的读写扩展方法 (推荐)
Provider
给我们的 BuildContext
扩展了读写之类的方法,
watch
获取的参数同时支持读写,不仅能默认展示内容,也能及时接收更新新内容
read
获取的变量能读取到信息,但无法接收更改的信息
@override
Widget build(BuildContext context) {
//推荐下面这么用,文档推荐,这么使用一定没错
//使用watch可以正常接收到更新的消息
final global = context.watch<GlobalNotifier>();
//如果监听依赖的可能会不存在,那么可以使用 ?
// final global = context.watch<GlobalNotifier?>();
// 使用read无法监听到更新的最新通知,但使用其更新数据,其他watch的地方仍然可以接收更改通知
// final global = context.read<GlabalNotifier>();
return Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
children: [
Text("测试内容2(watch):${global.user?.username}"),
Text("测试内容2(read):${context.read<GlobalNotifier>().user?.username}"),
],
),
TextButton(
onPressed: () {
//由于更新的是一个对象的一个属性,先获取后更新即可,如果是分散的属性,直接更新即可
final user = global.user;
if (user != null) {
user.username = "test2";
}
global.user = user;
},
child: const Text("更新"),
),
],
),
);
}
provider 定向更新局部参数,优化性能(推荐)
provider
更新时,会通知对应 Notifier
的所有参数,这时我们没有更新的方法也会触发 build 了,减少不必要的渲染,此时我们可以通过 select
定向获取某一个参数属性,避免不必要的渲染
如下所示,我们分别过滤出我们的 username 和 age,可以减少其他属性更新的干扰到这边重新执行 build
@override
Widget build(BuildContext context) {
//过滤其他更改,只更新对应属性,避免重新build widget
final username = context.select((GlobalNotifier e) => e.user?.username);
final age = context.select((GlobalNotifier e) => e.user?.age);
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("测试3(单属性name):$username"),
TextButton(
onPressed: () {
final global = context.read<GlobalNotifier>();
if (global.user != null) {
global.user!.username = "test3";
}
global.user = global.user;
},
child: const Text("更新"),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("测试3(单属性age):$age"),
TextButton(
onPressed: () {
final global = context.read<GlobalNotifier>();
if (global.user != null) {
global.user!.age = 20;
}
global.user = global.user;
},
child: const Text("更新"),
),
],
),
],
),
);
}
使用 Consumer 优化局部组件性能
Consumer
为一个性能优化部件,页面不大其实也没必要使用,其不仅能够接收到对应通知的变换,并及时反馈到组件上,且能保证该组件外部不会重新build
,只更新内部
包裹的组件,因此也算是不错的性能优化手段了
相比较 select
其更加优秀,但不冲突,可以根据必要两个都带,例如:Consumer
里面包裹的组件里面根据情况再使用 select
,也算是另一种极限优化了😂
此外,可能还会由于 Consumer
、Consumer2
、...、Consumer6
怎么这么多,是干什么的
先看看 Consumer
的 builder
就知道了,里面共有三个参数,第一个是 context,最后一个是 child,中间的就是我们泛型对应的值,那么 Consumer2
就是中间有两个,Consumer6
就是中间 6 个,这下明白了吧(ps:作者也是被泛型给耽误了,数组也是可以的哈)
Widget Function(
BuildContext context,
T value,
Widget? child,
)
Consumer
的应用案例如下所示
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//默认监听变化,跟watch一样
//context value1 child
Consumer<GlobalNotifier>(builder: (_, global, __) {
return Text("测试4(优化局部Consumer):${global.user?.username}");
}),
//同理,最多到 Consumer2,即6个参数
//context value1 value2 child
// Consumer2<GlobalNotifier, GlobalNotifier2>(builder: (_, global, global2, __) {
// return Text("测试4(优化局部控件刷新Consumer):${global.user?.username}");
// }),
TextButton(
onPressed: () {
final global = context.read<GlobalNotifier>();
if (global.user != null) {
global.user!.username = "测试4";
}
global.user = global.user;
},
child: const Text("更新"),
),
],
),
],
),
);
}
局部 Provider(局部某种意义上可能是某个模块的全局)
介绍局部 Provider
的时候,我们顺道也把 Provide.value
介绍了
前面介绍了,一个 Notifier
对应一个 Provider
,因此一个项目中一定会有多个 Provider
(这里面多个Provider
只是提供多个数据,使用上还是一样的)
除了前面的全局,其他大多数场景都是在局部了,局部 Provider
和 全部
的使用上一样
,唯一的不同
就是,Provider 根据泛型获取对应的 Notifier 时,会获取最局部的那个,也就是离他最近的
一个那个 Notifier
举个例子:在根部 MaterialApp
定义了一个 UserNotifier
,我们的个人页面也定义了一个 UserNotifier
,此时在个人模块获取到的 UserNotifier
就是个人页面定义声明的局部 UserNotifier
了,即每次进入到个人模块,用户都会使用局部的个人用户模块,如果假设全局给的是一个模糊的客户用户,那么进入个人模块获取就是一个精准的个人模块信息了
与此同时,某个模块局部 Provider
在另一个局部 Provider
面前,可能就相当于当前模块全局 Provider
了,就跟临时变量的作用域优先级似的,理解了这个关系,就容易灵活使用 Provider
了
场景一
、假设下面是我们的会员用户模块,每日进入我们都会重新初始化我们的会员参数,因此里面的 Notifier
参数相当于使用了一个全新的用户参数(如果同名的话)
//内外同时使用 同一个类型 value 时,会获取离得最近的 Provider 提供的内容
//不想使用前面的 provider 的话,可以直接返回child试试效果,相信马上会理解
//前面main里面也是这个global类型,这里也是,会获取到最近的局部 value
ChangeNotifierProvider(
create: (_) => GlobalNotifier(),
child: child,
);
场景二
、还是假设会员模块,我们的会员模块会根据根据接口返回的参数决定 Notifier
状态,且当前页面随时可能更新 Notifier
,因此为了方便 更新 Notifier
,我们可以使用 ChangeNotifierProvider.value
,以方便外部随时更新 Notifier
,从而推进内部组件更新
//外部声明一个 notifier
GlobalNotifier? notifier;
//内部使用,就不贴出函数了
//如果value由外部传入更新,最好使用 ChangeNotifierProvider.value 形式
//这样内外会使用同一个 value,更新时会得到同样的效果
return ChangeNotifierProvider.value(
value: notifier,
child: child,
);
多个Provider的写法
前面提到了一个 Notifier
对应一个 Provider
,如果需要多个参数,那么就需要多个 Provider
了,此时使用嵌套的方式,如果数量比较多会陷入嵌套地狱
,如下所示
//从上面也可以看出,我们引用中可能使用了多个 provider,提供多个数据
//如果全局数据比较多的话,可能会存在这种情况
test1() {
return ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
child: ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
child: ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
child: const Text(''),
),
),
);
}
provider
推出了 MultiProvider
,通过传递数组
的方式来对接多个 Provider
,来避免回调地狱
//通过数组代替嵌套,结果是一样的,是不是看着稍微舒服点了😂
test2() {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
),
ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
),
ChangeNotifierProvider(
create: (_) => GlobalNotifier2(),
),
],
child: const Text(''),
);
}
最后
使用代码就介绍这么多,实际上就已经完全够用了,可以下载 demo 运行试试,如果想深入理解,外面应该也有不少介绍原理的,原理上也没那么麻烦,可以多看看😂
转载自:https://juejin.cn/post/7167582299561656333