Flutter中如何通过Riverpod组合StateNotifierProvider与FutureProvider
问题核心原因
你当前拆分三个独立Provider的实现存在两个核心缺陷,导致重复触发定位请求:
- 独立的
currentFetchedLocationProvider只要被重新watch就会执行定位逻辑,没有内置缓存、请求复用判断,只要手动选中的位置被清空触发重建,就会重新调用定位接口,哪怕之前已经获取过有效位置 - 在非组件层直接读取
currentLocationProvider.future时,没有预先判断Provider当前是否已有缓存值,只要Provider处于非完成状态就会触发新的请求流程,无法复用正在进行的定位请求。
最优实现方案
直接使用Riverpod 2.x的AsyncNotifierProvider统一管理位置状态,不需要拆分多个Provider硬拼逻辑,天然支持AsyncValue的.when()状态区分能力,同时内置请求复用、状态缓存能力,完全匹配你的业务需求。
第一步:实现统一的位置状态Notifier
class CurrentLocationNotifier extends AsyncNotifier<LocationModel?> { @override Future<LocationModel?> build() async { // 初始化优先读取本地持久化的手动设置位置,存在则直接返回,不触发定位 final savedSelectedLocation = await _loadSavedManualLocation(); if (savedSelectedLocation != null) { return savedSelectedLocation; } // 无手动设置位置时才发起定位请求 return ref.read(locationServiceProvider).getCurrentLocation(); } /// 手动设置位置,优先级最高,设置后全局状态立即更新 Future<void> setManualLocation(LocationModel location) async { await _saveManualLocation(location); state = AsyncData(location); } /// 清除手动设置的位置,自动触发重新定位,组件会自动响应loading状态 Future<void> clearManualLocation() async { await _deleteSavedManualLocation(); state = const AsyncLoading(); // 用AsyncValue.guard自动捕获定位异常,统一进入error状态 state = await AsyncValue.guard( () => ref.read(locationServiceProvider).getCurrentLocation() ); } /// 定位失败重试方法 Future<void> retryLocation() async { state = const AsyncLoading(); state = await AsyncValue.guard( () => ref.read(locationServiceProvider).getCurrentLocation() ); } // 本地持久化逻辑,根据项目使用的存储方案(SharedPreferences/Hive/Isar等)替换实现即可 Future<LocationModel?> _loadSavedManualLocation() async { // 实现本地读取手动位置的逻辑 } Future<void> _saveManualLocation(LocationModel location) async { // 实现本地保存手动位置的逻辑 } Future<void> _deleteSavedManualLocation() async { // 实现本地删除手动位置的逻辑 } } // 全局Provider入口 final currentLocationProvider = AsyncNotifierProvider<CurrentLocationNotifier, LocationModel?>( CurrentLocationNotifier.new );
第二步:组件层使用
完全保留你需要的.when()状态区分能力,状态更新自动响应:
@override Widget build(BuildContext context, WidgetRef ref) { final locationState = ref.watch(currentLocationProvider); return locationState.when( loading: () => const CircularProgressIndicator(), error: (error, stackTrace) => Column( children: [ Text('定位失败:$error'), ElevatedButton( onPressed: () => ref.read(currentLocationProvider.notifier).retryLocation(), child: const Text('重试') ) ], ), data: (location) => Text('当前位置:${location?.address ?? "未知位置"}'), ); } // 手动设置/清除位置的调用示例 // 设置位置 ref.read(currentLocationProvider.notifier).setManualLocation(newLocation); // 清除位置 ref.read(currentLocationProvider.notifier).clearManualLocation();
第三步:非组件层(如DatabaseService)读取实现
完全满足你的读取要求:有缓存值直接返回不触发请求、请求中复用现有流程等待结果、无有效缓存时才触发新请求:
// 首先通过Provider注入DatabaseService,不要手动实例化传Reader final databaseServiceProvider = Provider<DatabaseService>((ref) { return DatabaseService(ref); }); class DatabaseService implements BaseDatabaseService { final Ref _ref; const DatabaseService(this._ref); Future<LocationModel?> get currentLocation async { // 先读当前同步状态,已有缓存数据直接返回,不触发任何重建/请求 final currentState = _ref.read(currentLocationProvider); if (currentState.hasValue) { return currentState.value; } // 处于loading/error状态时,等待现有请求完成,不会重复发起新定位 return _ref.read(currentLocationProvider.future); } Future<Map<String, dynamic>?> getBasicEventJson(String id) async { final location = await currentLocation; // 后续业务逻辑 } }
实现效果验证
- 手动设置位置优先级最高,设置后所有监听的组件、服务立即拿到最新值,不会触发定位
- 手动清除位置后,全局自动进入loading状态,定位完成后自动更新为新位置
- 重复调用
currentLocation取值时,只要已经有有效位置就直接返回,不会重复触发定位 - 定位过程中多次调用取值,只会发起一次定位请求,所有调用方共同等待同一个结果
- 完整支持loading/data/error三态区分,和普通FutureProvider/AsyncNotifier的使用体验完全一致
内容的提问来源于stack exchange,提问作者Florian Leeser
相关产品推荐
相关产品推荐

