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

Riverpod中FutureProvider重复调用致UI加载异常求助

问题

通过API获取国家数据,结合搜索框内容与搜索类型,使用Riverpod进行状态管理更新UI时遇到以下异常:

  • 当searchProvider有值时,UI始终处于加载状态,仅在搜索值清空后才加载完成;
  • 当searchProvider有值时,futureCountriesProviderFiltred中的print语句被频繁调用,直到搜索值清空才停止;
  • 即使简化futureCountriesProviderFiltred的代码逻辑,仍会出现重复调用与加载异常的问题。

核心代码:

final countriesProvider =
    StateNotifierProvider<CountriesProvider, List<Country>>((ref) {
  return CountriesProvider([]);
});

final futureCountriesProviderFiltred = FutureProvider<List<Country>>(
  (ref) async {
    final countries = ref.watch(countriesProvider);
    final searchType = ref.watch(searchTypeProvider);
    final search = ref.watch(searchProvider);
    print(search); // here
    print(countries.isEmpty);
    if (countries.isEmpty) {
      await ref.read(countriesProvider.notifier).fetchAndSetCountries();
      return countries;
    } else {
      if (searchType == SearchType.name) {
        await ref
            .read(countriesProvider.notifier)
            .fetchAndSetCountriesByName(search);
        return countries;
      } else {
        await ref
            .read(countriesProvider.notifier)
            .fetchAndSetCountriesByRegion(search);
        return countries;
      }
    }
  },
);

final searchTypeProvider = StateProvider<SearchType>((ref) {
  return SearchType.name;
});

enum SearchType { name, region }

final searchProvider = StateProvider<String>((ref) {
  return '';
});

class CountriesProvider extends StateNotifier<List<Country>> {
  final CountriesServices _countriesServices = CountriesServices();

  CountriesProvider(super.state);

  Future<void> fetchAndSetCountries() async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountries();
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }

  Future<void> fetchAndSetCountriesByName(String name) async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountriesByName(name);
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }

  Future<void> fetchAndSetCountriesByRegion(String region) async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountriesByRegion(region);
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }
}

UI代码:

Widget build(BuildContext context, WidgetRef ref) {
    final countriesProvider = ref.watch(futureCountriesProviderFiltred);   
    return Scaffold(
      appBar: AppBar(
        title: Center(
          child: SizedBox(
              child: SearchBar(
                onChanged: (value) {
                  ref.read(searchProvider.notifier).state = value;
                },
                trailing: [
                  SizedBox(
                    child: DropdownButton<String>(
                      items: <SearchType>[SearchType.name, SearchType.region]
                          .map<DropdownMenuItem<String>>((SearchType value) {
                        return DropdownMenuItem<String>(
                          value: value.toString(),
                          child: Text(
                            value.toString(),
                            style: TextStyle(fontSize: 15),
                          ),
                        );
                      }).toList(),
                      onChanged: (String? newValue) {
                        if (newValue == SearchType.name.toString())
                          ref.read(searchTypeProvider.notifier).state =
                              SearchType.name;
                        else
                          ref.read(searchTypeProvider.notifier).state =
                              SearchType.region;
                      },
                    ),
                  ),
                ],
                
              )),
        ),
      ),
      body: countriesProvider.when(
        data: (countries) {
          return ListView.builder(
            itemCount: countries.length,
            itemBuilder: (context, index) {
              return CountryCard(
                country: countries[index],
              );
            },
          );
        },
        loading: // ui for loading,
        error: (error, stackTrace) => Center(
          child: Text(error.toString()),
        ),
      ),
    );
}
解决方案

问题根源

  1. 循环触发FutureProvider:每次在futureCountriesProviderFiltred中调用fetchAndSetXXX方法时,会更新countriesProvider的状态,而futureCountriesProviderFiltred又监听了countriesProvider,导致FutureProvider反复重建,形成循环。
  2. 返回值错误:异步方法执行后直接返回旧的countries列表引用,没有等待状态更新后获取最新数据。
  3. 搜索输入频繁触发:SearchBar的onChanged每输入一个字符就更新searchProvider,导致FutureProvider频繁执行。

修改后的代码

状态管理部分

final countriesProvider = StateNotifierProvider<CountriesProvider, List<Country>>((ref) {
  return CountriesProvider([]);
});

// 单独处理初始数据加载,避免在过滤Provider中修改状态
final initialLoadProvider = FutureProvider<void>((ref) async {
  await ref.read(countriesProvider.notifier).fetchAndSetCountries();
});

final futureCountriesProviderFiltred = FutureProvider<List<Country>>((ref) async {
  // 等待初始加载完成
  await ref.watch(initialLoadProvider.future);
  
  final allCountries = ref.watch(countriesProvider);
  final searchType = ref.watch(searchTypeProvider);
  final search = ref.watch(debouncedSearchProvider).data?.value ?? '';

  if (search.isEmpty) {
    return allCountries;
  }

  // 本地过滤(如果需要远程搜索,直接调用API返回结果即可)
  if (searchType == SearchType.name) {
    return allCountries
        .where((country) => country.name.toLowerCase().contains(search.toLowerCase()))
        .toList();
  } else {
    return allCountries
        .where((country) => country.region.toLowerCase() == search.toLowerCase())
        .toList();
  }
});

final searchTypeProvider = StateProvider<SearchType>((ref) => SearchType.name);
enum SearchType { name, region }

final searchProvider = StateProvider<String>((ref) => '');

// 防抖处理搜索输入,避免频繁触发请求
final debouncedSearchProvider = FutureProvider<String>((ref) async {
  final search = ref.watch(searchProvider);
  await Future.delayed(const Duration(milliseconds: 300));
  return search;
});

class CountriesProvider extends StateNotifier<List<Country>> {
  final CountriesServices _countriesServices = CountriesServices();

  CountriesProvider(super.state);

  Future<void> fetchAndSetCountries() async {
    if (state.isNotEmpty) return; // 避免重复加载
    try {
      final loadedCountries = await _countriesServices.getCountries();
      state = loadedCountries;
    } catch (error) {
      // 这里可以添加错误提示逻辑
    }
  }
}

UI部分

Widget build(BuildContext context, WidgetRef ref) {
  // 监听防抖后的搜索值,间接触发过滤Provider更新
  ref.watch(debouncedSearchProvider);
  final countriesAsync = ref.watch(futureCountriesProviderFiltred);

  return Scaffold(
    appBar: AppBar(
      title: Center(
        child: SizedBox(
          child: SearchBar(
            onChanged: (value) {
              ref.read(searchProvider.notifier).state = value;
            },
            trailing: [
              SizedBox(
                child: DropdownButton<SearchType>(
                  value: ref.watch(searchTypeProvider),
                  items: SearchType.values.map((type) {
                    return DropdownMenuItem(
                      value: type,
                      child: Text(type.toString().split('.').last),
                    );
                  }).toList(),
                  onChanged: (SearchType? newValue) {
                    if (newValue != null) {
                      ref.read(searchTypeProvider.notifier).state = newValue;
                    }
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    ),
    body: countriesAsync.when(
      data: (countries) {
        return ListView.builder(
          itemCount: countries.length,
          itemBuilder: (context, index) => CountryCard(country: countries[index]),
        );
      },
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stackTrace) => Center(child: Text(error.toString())),
    ),
  );
}

关键优化点

  1. 拆分职责:用initialLoadProvider单独处理初始数据加载,切断futureCountriesProviderFiltred与状态修改的循环依赖。
  2. 防抖处理:新增debouncedSearchProvider,延迟300ms响应搜索输入,避免单次输入触发多次请求。
  3. 避免状态修改循环:过滤逻辑不再修改countriesProvider的状态,直接返回过滤结果,防止FutureProvider反复重建。
  4. 枚举优化:Dropdown直接使用SearchType枚举值,避免字符串转换的潜在错误。

内容的提问来源于stack exchange,提问作者TOZX

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 18:09:51