最近在项目里面碰到了使用Riverpod的FutureProvider引起的一例屏幕组件闪烁的问题,深入研究了使用FutureProvider可能的存在的问题及规避方法。
问题复现
页面里一个组件观察了FutureProviderA,根据FutureProviderA 返回的值决定组件是否展示,FutureProviderA 又watch了FutureProviderB,依赖结构如下

在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里面写太多业务逻辑
建议&总结
- 组件在使用FutureProvider的时候,使用select,稳定结果的值,避免loading态导致的无谓刷新
- FutureProvider里面避免依赖其他FutureProvider,否则可能会导致FutureProvider里面的逻辑多次执行
- 耗时获取数据等方法写到provider执行的外面,provider尽量只记录值的改变。业务逻辑可以内聚到Notifier里面,Notifier处理完之后赋值给Provider