Flutter 中自定义一个简单的Provider
Provider
是我们在做Flutter
项目中经常使用的状态管理工具,它为我们在两个不相关的组件中传值提供了很大的方便,今天我们来按照Provider的思路实现一个简版的MyProvider
。
一、首先我们先来看一下正宗的三方库Provider是如何使用和实现不同页面间状态管理的。
新建一个项目,在pubspec.yaml
文件中引入provider
依赖,

接下来做两个简单的页面,首页编辑个人信息,我的页面展示个人信息,通过这两个页面演示两个不同页面的数据编辑和更新。

主要代码如下:
1、在MaterialApp
外面包裹ChangeNotifierProvider
,并在create
方法中传入我们的UserInfoModel
模型类

2、新建TabBarPage
作为首页编辑页面和我的个人信息展示页面的父组件:

3、首页代码:两个输入框编辑信息,一个 Switch
切换按钮 控制是否隐藏手机号,并且混入AutomaticKeepAliveClientMixin
类,确保页面切换时保持页面状态
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin{
@override
bool get wantKeepAlive => true;
final TextEditingController _nameController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
@override
Widget build(BuildContext context) {
final userModel = context.watch<UserInfoModel>();
_nameController.text = userModel.name;
_phoneController.text = userModel.phone;
bool showPhone = userModel.showPhone;
return Scaffold(
appBar: AppBar(
title:const Text("首页-设置个人信息"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
const Text("姓名:"),
Expanded(
child: CupertinoTextField(
onChanged: (v) => userModel.name = v,
controller: _nameController,
placeholder: "请输入姓名",
))
],
),
const SizedBox(height: 20,),
Row(
children: [
const Text("手机号:"),
Expanded(
child: CupertinoTextField(
maxLength: 11,
onChanged: (v) => userModel.phone = v,
controller: _phoneController,
placeholder: "手机号",
),
)
],
),
const SizedBox(height: 20,),
Row(
children: [
const Text("隐藏手机号:"),
Switch(
value: showPhone,
onChanged: (val) {
setState(() => userModel.showPhone = val);
}),
],
),
],
),
),
);
}
}
4、我的页面,展示个人信息,同样混入AutomaticKeepAliveClientMixin
保持页面状态
class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin{
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
final userModel = context.watch<UserInfoModel>();
return Scaffold(
appBar: AppBar(
title: const Text("个人中心-展示个人信息"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListTile(
title: const Text('姓名'),
trailing: Text(userModel.name, style: const TextStyle(fontSize: 20),),
),
ListTile(
title: const Text('手机号'),
trailing: Text(userModel.showPhone
? userModel.phone
: userModel.phone.replaceRange(3, userModel.phone.length,
'*' * (userModel.phone.length - 3)),style: const TextStyle(fontSize: 20)),
),
],
),
),
);
}
}
5、UserInfoModel
模型类:

以上就是使用三方Provider完成的跨页面状态管理,使用起来还是比较简单的,下面我们把三方依赖库从pubspec.yaml 文件中删除,开始来一步步实现我们自己的Provider
二、实现自己的Provier
我们都知道Provider
主要是借助InheritedWidget
,我们先从main.dart
开始,将三方库的ChangeNotifierProvider 改成自己定义的继承于InheritedWidget
的类,并且包含一个model
属性,由于我们现在的这个类是继承与InheritedWidget
,我们暂且把这个新的类起个名字叫MyInheritedWidget
:

HomePage
中获取userModel
的方式需要改一下,以前我们是这样获取的:
final userModel = context.watch<UserInfoModel>();
现在我们不能这么方便的获取model
了,但是我们可以使用这种方式
context.dependOnInheritedWidgetOfExactType
所以HomePage
改动一行代码:

同样的,MinePage
也需要改动一下:

这样就可以了吗?运行一下项目

发现并没有像以前一样,在首页改动后,我的页面的信息根本没有更新,我们来分析一下原因,当首页的值被修改以后,因为UserInfoModel
是继承自ChangeNotifier
,我们在这个类中重写了属性的set 方法,当UserInfoModel
的值被修改后,我们调用了notifyListeners
,去通知监听UserInfoModel
的地方,告诉他属性变了,该更新了,(这个机制有点类似于iOS
原生种通知中心),但是我们在代码中并没有监听属性值的改变,我们在使用三方Provider
的时候,Provider
为我们在页面中添加了监听,所以我们也应该在页面中加一个监听属性变化的组件,在新版Flutter
中使用ListenableBuilder
,(在旧版本中使用AnimatedBuilder
,除了名字跟ListenableBuilder
和 属性名 animation
(被监听的对象) 不同以外,用法和原理都是一模一样的),所以在首页和我的页面最外层分别包裹一个ListenableBuilder
:


Hot Reload,发现两个页面的状态又可以同步更新了:

三、改进
对比三方库的Provider
,我们发现我们自己的MyInheritedWidget
使用起来太繁琐了,首先Provider
在使用的时候并没有在每个组件外层包裹一个ListenableBuilder
组件;其次Provider
在获取model
的时候直接使用context.watch
方法,不像我们使用context.dependOnInheritedWidgetOfExactType
这么长的方法;最后Provider在MaterialAPP
外面包裹了ChangeNotifierProvider
传入的是create
方法,我们传入的是model
。
针对以上问题,我们逐步来完善。
我们页面中每个需要监听的地方都需要包裹一个ListenableBuilder
,但是我们明明已经在使用InheritedWidget
,并且使用context.dependOnInheritedWidgetOfExactType
来监听了,为什么还要包裹一个ListenableBuilder
来监听呢?
我们来分析一下,首先在updateShouldNotify
方法中打印一个日志:

重新运行后更改状态信息,我们发现这个地方只在第一次运行时打印了,后面再改动状态,这里根本就没有更新,也就是说新的InheritedWidget
替换oldWidget
这个动作没有发生,怎样才能让它更新呢?我们需要在MyInheritedWidget
上层,在MyApp
的下面包裹一个有状态的组件,调用setState
,如此来更新MyInheritedWidget
,从而更新状态数据,所以我们需要新建一个有状态的类,我们起个名字叫MyProvider
吧,把model
和child
传进来,并且把在之前每个组件中的ListenableBuilder
包裹在child
的外层:
class MyProvider extends StatefulWidget {
const MyProvider({required this.model, required this.child, super.key});
final UserInfoModel model;
final Widget child;
@override
State<MyProvider> createState() => _MyProviderState();
}
class _MyProviderState extends State<MyProvider> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.model,
builder: (context, child) {
return MyInheritedWidget(model: widget.model, child: widget.child);
}
);
}
}
现在MyApp 就变成这样:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MyProvider(
model: UserInfoModel(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TabBarPage(),
),
);
}
}
另外我们也像Provider
那样把model
通过一个create
方法传进来,在MyProvider
的State
中定义一个model
,在initState
方法中实例化model,现在MyProvider
就是这样了:
class MyProvider extends StatefulWidget {
const MyProvider({required this.create, required this.child, super.key});
final Function create;
final Widget child;
@override
State<MyProvider> createState() => _MyProviderState();
}
class _MyProviderState extends State<MyProvider> {
late UserInfoModel model;
@override
void initState() {
super.initState();
model = widget.create();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: model,
builder: (context, child) {
return MyInheritedWidget(model: model, child: widget.child);
}
);
}
}
另外再提一句,Provider
把model
放在一个create
方法中实例化也是有原因的,这样我们可以把model
放在State
中,后面有状态刷新的话model
是跟随State
的生命周期,刷新时可以被复用,保证状态不会丢失,而Widget
是不可变的,随时会被替换。
现在我们这个MyProvider
跟原来的Provider
就有点像了
还有一个我们上面提到的,Provider
获取模型数据是context.get
,而我们是使用context.dependOnInheritedWidgetOfExactType
这么长的语句,这是是Dart
中的extension
:
extension Comsumer on BuildContext {
UserInfoModel watch (){
return dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!.model;
}
UserInfoModel get (){
return getInheritedWidgetOfExactType<MyInheritedWidget>()!.model;
}
}
然后页面中就可以像provider
这样通过watch
获取数据:
最后我们再把泛型改造一下,可以支持其他model
,就跟provider
一样了,代码再整理一下,放在github上吧
转载自:https://juejin.cn/post/7364614337371717671