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()), ), ), ); }
解决方案
问题根源
- 循环触发FutureProvider:每次在
futureCountriesProviderFiltred中调用fetchAndSetXXX方法时,会更新countriesProvider的状态,而futureCountriesProviderFiltred又监听了countriesProvider,导致FutureProvider反复重建,形成循环。 - 返回值错误:异步方法执行后直接返回旧的
countries列表引用,没有等待状态更新后获取最新数据。 - 搜索输入频繁触发: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())), ), ); }
关键优化点
- 拆分职责:用
initialLoadProvider单独处理初始数据加载,切断futureCountriesProviderFiltred与状态修改的循环依赖。 - 防抖处理:新增
debouncedSearchProvider,延迟300ms响应搜索输入,避免单次输入触发多次请求。 - 避免状态修改循环:过滤逻辑不再修改
countriesProvider的状态,直接返回过滤结果,防止FutureProvider反复重建。 - 枚举优化:Dropdown直接使用
SearchType枚举值,避免字符串转换的潜在错误。
内容的提问来源于stack exchange,提问作者TOZX
相关产品推荐
相关产品推荐

