ListView滚动时Riverpod FutureProvider被销毁问题排查
ListView滚动时Riverpod的FutureProvider重复刷新问题排查与解决
我在使用Riverpod开发Flutter应用时遇到一个问题:当ListView滚动时,自定义的getBanners FutureProvider会被销毁并重新请求数据。以下是相关代码实现:
Provider定义
@riverpod Future<List<BannerModel>> getBanners(Ref ref, {String? type}) async { final result = await ref .watch(bannerRepositoryProvider) .getBanners(type: type); return result.fold( (failure) => throw Exception(failure.message), (banners) => banners, ); }
MainSliderWidget代码
class MainSliderWidget extends HookConsumerWidget { const MainSliderWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final carouselController = useMemoized(() => CarouselSliderController()); final mainSlider = ref.watch(getBannersProvider(type: "main")); final current = useState(0); return mainSlider.when( data: (sliderItems) => Column( children: [ CarouselSlider( carouselController: carouselController, items: sliderItems.map((e) { return Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: CachedNetworkImage( imageUrl: "$storageUrl${e.resourcePath}", placeholder: (context, url) => SpinKitDualRing(color: Colors.blue), errorWidget: (context, url, error) => const Icon(Icons.error), imageBuilder: (context, imageProvider) { return Image( image: imageProvider, fit: BoxFit.fill, width: double.infinity, height: double.infinity, ); }, ), ), ); }).toList(), options: CarouselOptions( onPageChanged: (index, reason) => current.value = index, height: 150, viewportFraction: 0.9, autoPlay: true, enlargeCenterPage: true, ), ), ], ), error: (error, stackTrace) => Text("hello"), loading: () => Column( children: [ CarouselSlider( carouselController: carouselController, items: [0,1,2,3,4].map((e) { return Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: ShimmerEffect( decoration: BoxDecoration(color: Colors.grey), ), ), ); }).toList(), options: CarouselOptions( height: 150, viewportFraction: 0.9, autoPlay: true, enlargeCenterPage: true, ), ), ], ), ); } }
HomeScreen代码
@RoutePage() class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final language = ref.watch(languageSharedPrefsNotifierProvider); final banners = ref.watch(getBannersProvider(type: language)); return ResponsiveWidget( appBar: HomeAppbar(), child: RefreshIndicator( onRefresh: () => Future.wait([]), child: ListView( padding: EdgeInsets.only(bottom: 100), children: [ const Gap(20), const MainSliderWidget(), const Gap(20), const ShopByCategorySectionWidget(), const Gap(20), banners.maybeWhen( data: (bannersList) => BannerItemWidget(link: bannersList[0].resourcePath), orElse: () => Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Container( height: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: ShimmerEffect( decoration: BoxDecoration(color: Colors.grey), ), ), ), ), ), Gap(20), ShopByBrandSectionWidget(), Gap(20), ], ), ), ); } }
问题原因
这不是Riverpod本身的问题,是你的实现方式导致的,核心两点:
- ListView的子widget销毁重建:ListView默认会销毁滚出视口的子widget,当再次滚回时会重新build。
MainSliderWidget内部直接调用ref.watch,每次重建都会重新监听Provider,触发数据请求。 - Provider默认缓存策略:FutureProvider在没有监听者时会自动清理缓存,当
MainSliderWidget被销毁后,Provider失去监听者,缓存被删除,下次重建时就会重新发起请求。
解决方案
方法一:让Provider持久化缓存
修改getBanners Provider,添加keepAlive配置,让它即使没有监听者也保留缓存:
@riverpod Future<List<BannerModel>> getBanners(GetBannersRef ref, {String? type}) async { // 开启持久化缓存,避免无监听时销毁数据 ref.keepAlive(); final result = await ref .watch(bannerRepositoryProvider) .getBanners(type: type); return result.fold( (failure) => throw Exception(failure.message), (banners) => banners, ); }
方法二:将Provider监听移到父widget
在HomeScreen中提前监听getBannersProvider(type: "main"),把数据传递给MainSliderWidget,避免子widget重建触发重新请求:
修改HomeScreen
@RoutePage() class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final language = ref.watch(languageSharedPrefsNotifierProvider); final banners = ref.watch(getBannersProvider(type: language)); // 提前监听main类型的banner,缓存数据 final mainBanners = ref.watch(getBannersProvider(type: "main")); return ResponsiveWidget( appBar: HomeAppbar(), child: RefreshIndicator( onRefresh: () => Future.wait([]), child: ListView( padding: EdgeInsets.only(bottom: 100), children: [ const Gap(20), // 将缓存的数据传递给子组件 MainSliderWidget(mainBanners: mainBanners), const Gap(20), const ShopByCategorySectionWidget(), const Gap(20), banners.maybeWhen( data: (bannersList) => BannerItemWidget(link: bannersList[0].resourcePath), orElse: () => Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Container( height: 100, decoration: BoxDecoration(borderRadius: BorderRadius.circular(15)), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: ShimmerEffect(decoration: BoxDecoration(color: Colors.grey)), ), ), ), ), const Gap(20), const ShopByBrandSectionWidget(), const Gap(20), ], ), ), ); } }
修改MainSliderWidget
class MainSliderWidget extends HookConsumerWidget { const MainSliderWidget({super.key, required this.mainBanners}); final AsyncValue<List<BannerModel>> mainBanners; @override Widget build(BuildContext context, WidgetRef ref) { final carouselController = useMemoized(() => CarouselSliderController()); final current = useState(0); return mainBanners.when( data: (sliderItems) => Column( children: [ CarouselSlider( carouselController: carouselController, items: sliderItems.map((e) { return Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration(borderRadius: BorderRadius.circular(15)), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: CachedNetworkImage( imageUrl: "$storageUrl${e.resourcePath}", placeholder: (context, url) => SpinKitDualRing(color: Colors.blue), errorWidget: (context, url, error) => const Icon(Icons.error), imageBuilder: (context, imageProvider) => Image( image: imageProvider, fit: BoxFit.fill, width: double.infinity, height: double.infinity, ), ), ), ); }).toList(), options: CarouselOptions( onPageChanged: (index, reason) => current.value = index, height: 150, viewportFraction: 0.9, autoPlay: true, enlargeCenterPage: true, ), ), ], ), error: (error, stackTrace) => const Text("加载失败"), loading: () => Column( children: [ CarouselSlider( carouselController: carouselController, items: [0,1,2,3,4].map((e) { return Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration(borderRadius: BorderRadius.circular(15)), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: ShimmerEffect(decoration: BoxDecoration(color: Colors.grey)), ), ); }).toList(), options: CarouselOptions( height: 150, viewportFraction: 0.9, autoPlay: true, enlargeCenterPage: true, ), ), ], ), ); } }
另外,也可以给ListView添加addAutomaticKeepAlives: true,但该配置对StatefulWidget更有效,结合上面的方法使用效果更佳。
内容的提问来源于stack exchange,提问作者Fahmi Sawalha
相关产品推荐
相关产品推荐

