likes
comments
collection
share

flutter-provider使用

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

前言

前面简单讲了 InheritedWidget,其为我们解决了基础数据共享问题,但跨页面状态管理很不方便,比较适合静态数据, 因此 provider 应运而生

provider 其基于 InheritedWidget,数据共享问题肯定ok,与此同时,还方便了我们跨多个页面进行数据交互,使用灵活,且对于 build 调用比较频繁的 Widget,其也有优化措施,能够减少调用,可以说非常优秀,并且其基于 InheritedWidget,因此其也是最接近 flutter 风格的一款跨组件的状态解决方案了

ps: 顺便提一下,getx 一把嗦一时爽,一直嗦一直爽,同时其也面对着另一个问题,开发风格已经脱离了原本的 flutter 风格,初级开发很容易对其过度依赖,这样对 flutter 的理解,以及后续组件生态会存在一个断层,毕竟很多开发者组件是要减少依赖的,不会基于 getx 进行开发,因此 provider 亦是一个不错的持续解决方案(这我两个都看了并尝试,让我感觉更接近flutter的就是他了,因此写文章以及后续使用也会是他,还有另外几个个人感觉已经被过渡过去了,可以舍弃)

provider地址

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,也算是另一种极限优化了😂

此外,可能还会由于 ConsumerConsumer2、...、Consumer6怎么这么多,是干什么的

先看看 Consumerbuilder 就知道了,里面共有三个参数,第一个是 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 运行试试,如果想深入理解,外面应该也有不少介绍原理的,原理上也没那么麻烦,可以多看看😂