Flutter Riverpod加载状态问题:如何实现全局覆盖式加载而非局部Widget
问题描述
在Flutter应用中使用Riverpod进行状态管理时,处理加载状态遇到了问题:希望实现全局覆盖式的加载效果,但当前通过Riverpod的when方法返回的加载Widget仅显示在Widget树的局部位置,无法让整个UI处于等待加载的状态。当前代码如下:
_getVideoData().when( data: (data) { if (data != null) { if (_isInterstitialLoaded) { _interstitialAd.show(); } _showVideo(data.url); return VideoPlayerWidget( videoPlayerController: videoPlayerController, videoDescription: data.description, authorNickname: data.authorNickname, avatar: data.avatar, downloadOnPressed: () async { if (storagePermission.isGranted) { await _saveVideo(data); } if (!storagePermission.isGranted) { if (storagePermission.canRequestPermission) { await storagePermission.checkPermission(); } else { await openAppSettings(); } } }, ); } return const SizedBox.shrink(); }, error: (error, _) { return const Center( child: Text('Deneme'), ); }, loading: () { return Image.asset( 'assets/gifs/hourglass.gif', ); }, )
解决方案
方法1:用Stack包裹全局UI
将页面完整UI与加载遮罩放在Stack中,通过Riverpod的状态控制遮罩的显示:
Consumer( builder: (context, ref, child) { // 先将_getVideoData转为FutureProvider,才能用ref.watch监听 final videoDataAsync = ref.watch(_getVideoDataProvider); return Stack( children: [ // 页面原有UI,加载时可选择隐藏或保留基础框架 videoDataAsync.when( data: (data) { if (data != null) { // 将副作用逻辑放到postFrameCallback,避免在build中执行 WidgetsBinding.instance.addPostFrameCallback((_) { if (_isInterstitialLoaded) _interstitialAd.show(); _showVideo(data.url); }); return VideoPlayerWidget( videoPlayerController: videoPlayerController, videoDescription: data.description, authorNickname: data.authorNickname, avatar: data.avatar, downloadOnPressed: () async { if (storagePermission.isGranted) { await _saveVideo(data); } if (!storagePermission.isGranted) { if (storagePermission.canRequestPermission) { await storagePermission.checkPermission(); } else { await openAppSettings(); } } }, ); } return const SizedBox.shrink(); }, error: (error, _) => const Center(child: Text('Deneme')), loading: () => const SizedBox.shrink(), ), // 全局加载遮罩,仅在加载状态显示 if (videoDataAsync.isLoading) Container( color: Colors.black54, // 半透明背景增强遮罩效果 width: double.infinity, height: double.infinity, child: Center( child: Image.asset('assets/gifs/hourglass.gif'), ), ), ], ); }, )
方法2:利用Overlay插入全局加载组件
通过Flutter的Overlay机制,在加载状态时动态插入全局遮罩:
// 先定义一个Provider保存OverlayEntry,用于后续移除遮罩 final _overlayEntryProvider = StateProvider<OverlayEntry?>((ref) => null); // 页面中使用Consumer监听状态 Consumer( builder: (context, ref, child) { final videoDataAsync = ref.watch(_getVideoDataProvider); // 监听状态变化,控制遮罩的显示与移除 ref.listen(_getVideoDataProvider, (previous, next) { if (next.isLoading) { final overlayEntry = OverlayEntry( builder: (context) => Container( color: Colors.black54, width: double.infinity, height: double.infinity, child: Center( child: Image.asset('assets/gifs/hourglass.gif'), ), ), ); Overlay.of(context).insert(overlayEntry); ref.read(_overlayEntryProvider.notifier).state = overlayEntry; } else { final entry = ref.read(_overlayEntryProvider); entry?.remove(); ref.read(_overlayEntryProvider.notifier).state = null; } }); return videoDataAsync.when( data: (data) { if (data != null) { if (_isInterstitialLoaded) _interstitialAd.show(); _showVideo(data.url); return VideoPlayerWidget(...); // 原组件代码 } return const SizedBox.shrink(); }, error: (error, _) => const Center(child: Text('Deneme')), loading: () => const SizedBox.shrink(), ); }, )
方法3:封装通用全局加载组件
封装一个可复用的全局加载组件,结合Riverpod状态快速实现效果:
class GlobalLoader extends StatelessWidget { final Widget child; final AsyncValue<void> loadingState; const GlobalLoader({ super.key, required this.child, required this.loadingState, }); @override Widget build(BuildContext context) { return Stack( children: [ child, if (loadingState.isLoading) Container( color: Colors.black54, width: double.infinity, height: double.infinity, child: Center( child: Image.asset('assets/gifs/hourglass.gif'), ), ), ], ); } }
使用示例:
Consumer( builder: (context, ref, child) { final videoDataAsync = ref.watch(_getVideoDataProvider); return GlobalLoader( loadingState: videoDataAsync, child: videoDataAsync.when( data: (data) { if (data != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_isInterstitialLoaded) _interstitialAd.show(); _showVideo(data.url); }); return VideoPlayerWidget(...); // 原组件代码 } return const SizedBox.shrink(); }, error: (error, _) => const Center(child: Text('Deneme')), loading: () => const SizedBox.shrink(), ), ); }, )
关键注意点
- 不要在
when的data回调中直接执行副作用(如展示广告、初始化视频),应放到addPostFrameCallback或ref.listen中,避免build方法重复执行导致重复操作。 - 确保
_getVideoData是Riverpod的Provider(如FutureProvider),这样才能通过ref.watch或ref.listen监听状态变化。 - 全局遮罩建议添加半透明背景,提升用户体验,防止加载时底层UI被误触。
内容的提问来源于stack exchange,提问作者themmfa
相关产品推荐
相关产品推荐

