likes
comments
collection
share

Flutter 中自定义一个简单的Provider

作者站长头像
站长
· 阅读数 25

Provider 是我们在做Flutter项目中经常使用的状态管理工具,它为我们在两个不相关的组件中传值提供了很大的方便,今天我们来按照Provider的思路实现一个简版的MyProvider

一、首先我们先来看一下正宗的三方库Provider是如何使用和实现不同页面间状态管理的。 新建一个项目,在pubspec.yaml文件中引入provider 依赖,

Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider

主要代码如下:

1、在MaterialApp外面包裹ChangeNotifierProvider,并在create 方法中传入我们的UserInfoModel 模型类

Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider

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 模型类:

Flutter 中自定义一个简单的Provider

以上就是使用三方Provider完成的跨页面状态管理,使用起来还是比较简单的,下面我们把三方依赖库从pubspec.yaml 文件中删除,开始来一步步实现我们自己的Provider

二、实现自己的Provier

我们都知道Provider主要是借助InheritedWidget ,我们先从main.dart 开始,将三方库的ChangeNotifierProvider 改成自己定义的继承于InheritedWidget 的类,并且包含一个model 属性,由于我们现在的这个类是继承与InheritedWidget,我们暂且把这个新的类起个名字叫MyInheritedWidget

Flutter 中自定义一个简单的Provider

HomePage中获取userModel 的方式需要改一下,以前我们是这样获取的:

final userModel = context.watch<UserInfoModel>();

现在我们不能这么方便的获取model了,但是我们可以使用这种方式

context.dependOnInheritedWidgetOfExactType

所以HomePage 改动一行代码:

Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider Flutter 中自定义一个简单的Provider

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

Flutter 中自定义一个简单的Provider

三、改进

对比三方库的Provider,我们发现我们自己的MyInheritedWidget 使用起来太繁琐了,首先Provider在使用的时候并没有在每个组件外层包裹一个ListenableBuilder组件;其次Provider在获取model的时候直接使用context.watch方法,不像我们使用context.dependOnInheritedWidgetOfExactType这么长的方法;最后Provider在MaterialAPP外面包裹了ChangeNotifierProvider传入的是create方法,我们传入的是model。 针对以上问题,我们逐步来完善。

我们页面中每个需要监听的地方都需要包裹一个ListenableBuilder,但是我们明明已经在使用InheritedWidget,并且使用context.dependOnInheritedWidgetOfExactType来监听了,为什么还要包裹一个ListenableBuilder来监听呢? 我们来分析一下,首先在updateShouldNotify 方法中打印一个日志:

Flutter 中自定义一个简单的Provider

重新运行后更改状态信息,我们发现这个地方只在第一次运行时打印了,后面再改动状态,这里根本就没有更新,也就是说新的InheritedWidget 替换oldWidget这个动作没有发生,怎样才能让它更新呢?我们需要在MyInheritedWidget上层,在MyApp的下面包裹一个有状态的组件,调用setState,如此来更新MyInheritedWidget,从而更新状态数据,所以我们需要新建一个有状态的类,我们起个名字叫MyProvider吧,把modelchild传进来,并且把在之前每个组件中的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方法传进来,在MyProviderState中定义一个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);
      }
    );
  }
}

另外再提一句,Providermodel放在一个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 获取数据:

Flutter 中自定义一个简单的Provider

最后我们再把泛型改造一下,可以支持其他model,就跟provider一样了,代码再整理一下,放在github上吧

转载自:https://juejin.cn/post/7364614337371717671
评论
请登录