FutureProvider会刷新两次的问题研究

最近在项目里面碰到了使用Riverpod的FutureProvider引起的一例屏幕组件闪烁的问题,深入研究了使用FutureProvider可能的存在的问题及规避方法。

问题复现

页面里一个组件观察了FutureProviderA,根据FutureProviderA 返回的值决定组件是否展示,FutureProviderAwatchFutureProviderB,依赖结构如下

在app启动的时候,FutureProviderB由于某种原因会刷新很多次导致Widget会闪烁

Widget代码

kotlin 复制代码
child: futureProviderA.when(
      data: (data) {
        return Text(context, data);
      }, error: (Object error, StackTrace stackTrace) {
        return null;
      }, loading: () {
        return null;
      }),
)),

上面Widget在观察FutureProviderA,在when里面处理了三种情况,只有在data的情况下返回Text组件,其他情况返回null。

解决方法

在when里面加一个参数skipLoadingOnReload: true即可。

FutureProvider返回的是AsyncValue,这个类有三种状态

  • loading:代表数据正在加载
  • data:表示新数据已经加载好了
  • error:数据加载错误

AsyncValue.when方法有两个参数

arduino 复制代码
//是否在reload时跳过loading状态,也就是ref.watch的时候,如果watch的对象改变,会导致
//当前provider刷新进入loading,此时如果跳过loading状态,就不回调loading,而是
//调用error或者data,默认情况下不跳过
bool skipLoadingOnReload = false, 

//是否在refresh的情况下跳过loading,也就是ref.invalidate时,provider进入loading状态
//此时是否调用loading,默认是跳过,如果没有错误,一般调用data
bool skipLoadingOnRefresh = true, 

再结合when的源码来看就比较清晰为什么会出现闪烁了,因为skipLoadingOnReload为false,ProviderB刷新了多次,导致进入loading多次,而我们在loading的时候是返回null的,因此会多次闪烁。而把skipLoadingOnReload改成true,在ProviderB刷新的时候,loading态调用的是data,也就是返回实际的组件,因此不会闪烁

ini 复制代码
R when<R>({
  bool skipLoadingOnReload = false,
  bool skipLoadingOnRefresh = true,
  bool skipError = false,
  required R Function(T data) data,
  required R Function(Object error, StackTrace stackTrace) error,
  required R Function() loading,
}) {
  if (isLoading) {
    bool skip;
    if (isRefreshing) {
      skip = skipLoadingOnRefresh;
    } else if (isReloading) {
      skip = skipLoadingOnReload;
    } else {
      skip = false;
    }
    if (!skip) return loading();
  }

  if (hasError && (!hasValue || !skipError)) {
    return error(this.error!, stackTrace!);
  }

  return data(requireValue);
}

FutureProvider导致组件build两次问题

上面的问题是解决了,但是存在多次调用build的情况,为了解决这个,需要深入了解FutureProvider的使用方法

在下面的例子里面widget观察一个FutureProvider,provider在内部会delay一秒,然后返回true。我们把关键的链路上都加上日志,观察一下FutureProvider的行为

示例代码

less 复制代码
class MyApp extends StatelessWidget {
  MyApp({super.key});

  ///------------数据生产侧-----------------
  final futureProvider = FutureProvider<bool>(
    (ref) async {
      print('tagtag provider刷新');
      await Future.delayed(const Duration(seconds: 1));
      return true;
    },
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text('例子'),
      ),
      ///------------消费侧-----------------
      body: Consumer(builder: (context, ref, _) {
        print('tagtag build');
        var value = ref.watch(futureProvider).when(data: (d) {
          print('tagtag on data ${d}');
          return d;
        }, error: (_, stack) {
          print('tagtag on error');
        }, loading: () {
          print('tagtag on loading');
        });
        print('tagtag value is $value');
        return ElevatedButton(
            onPressed: () {
              ref.invalidate(futureProvider);
            },
            child: Text("点击"));
      }),
    );
  }
}

打开页面第一次启动日志

csharp 复制代码
--------第一次打印内容---------
tagtag build
tagtag provider刷新
tagtag on loading

--------1秒之后打印内容---------
tagtag build
tagtag on data true

上面的第一次日志很明显,FutureProvider进入loading状态,并调用了loading。1秒之后有值了,调用data

点击刷新,调用invalidate之后日志

csharp 复制代码
--------第一次打印内容---------
tagtag provider刷新
tagtag build
tagtag on data true

--------1秒之后打印内容---------
tagtag build
tagtag on data true

上面因为我们是invalidate刷新provider,并且默认我们是跳过loading态的,因此进入loading状态时,调用的是data,1秒之后数据回来了就会进入data状态,调用data回调

在这个例子里面会发现组件被build了两次,这显然不是我们想要的。怎么才能只build一次?

使用provider的selector,上面的watch改成

csharp 复制代码
ref.watch(futureProvider.select((selector) => selector.value)) ??
    false;

select有稳定的作用,只要select里面函数的返回值不变,那么就不会触发重新build,上面的例子,不论点击刷新多少次都不会导致build,因为每次都返回true。

FutureProvider依赖引发的多次调用

还有一种情况就是FutureProviderA依赖FutureProviderB,如果B发生变化,那么就会重建,而且会重建多次,如果在A里面调用了接口,那么就会引发一些副作用。因此要不然就在A里面使用上面说的select,要不然就不要在provider里面写太多业务逻辑

建议&总结

  1. 组件在使用FutureProvider的时候,使用select,稳定结果的值,避免loading态导致的无谓刷新
  2. FutureProvider里面避免依赖其他FutureProvider,否则可能会导致FutureProvider里面的逻辑多次执行
  3. 耗时获取数据等方法写到provider执行的外面,provider尽量只记录值的改变。业务逻辑可以内聚到Notifier里面,Notifier处理完之后赋值给Provider
相关推荐
天蓝色的鱼鱼2 小时前
Next.js路由全解析:Pages Router 与 App Router,你选对了吗?
前端·next.js
xun_xing2 小时前
基于Nextjs15的学习手记
前端·javascript·react.js
有意义2 小时前
Vibe Coding:人机共生时代的开发革命 —— 从概念到 Chrome 扩展实战
前端·ai编程·vibecoding
梅梅绵绵冰2 小时前
SpringMVC快速入门
前端
kirkWang2 小时前
HarmonyOS 6.0 服务卡片实战:把「轻食刻」装进桌面,让轻断食一眼可控
前端
1024小神2 小时前
VNBarcodeObservation的结果中observation.boundingBox 是什么类型?
前端
xun_xing2 小时前
Javascript的Iterator和Generator
前端·javascript
秃了才能变得更强2 小时前
React Native 新、旧架构集成原生模块方式
前端
1024小神2 小时前
swift中VNDetectBarcodesRequest VNImageRequestHandler 是什么?有什么作用?VN是什么意思
前端