You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:24:51