如何避免Flutter Bloc场景下相同用户信息重复发起网络请求?
问题根因
你遇到的重复请求问题本质是两个场景的叠加:
- 竞态条件:两个Avatar组件同时初始化时,几乎同时调用
getUserInfo方法,此时第一个网络请求还未完成,缓存尚未写入,因此两次调用都会判断本地无缓存,同时发起网络请求。 - 现有Bloc设计缺陷:你的UserInfoBloc目前仅支持存储单个用户的状态,若同时渲染多个不同用户的Avatar,还会出现状态互相覆盖的问题。
解决方案
1. 修复Repository层的竞态问题
新增正在请求中的任务缓存,避免同一个userId同时发起多次重复请求,修改后的UserInfoRepositoryImpl代码如下:
@LazySingleton(as: UserInfoRepository) class UserInfoRepositoryImpl extends UserInfoRepository { final GetUserInfoRemoteDataSource remoteDataSource; final GetUserInfoLocalDataSource localDataSource; // 存储正在进行中的用户信息请求,key为userId,value为对应的请求Future final Map<int, Future<Either<Failure, User>>> _pendingRequests = {}; UserInfoRepositoryImpl({ required this.remoteDataSource, required this.localDataSource }); @override Future<Either<Failure, User>> getUserInfo(int id) async { try { // 优先查本地缓存 final existsInCache = localDataSource.containsUserModel(id); if (existsInCache) { return right(localDataSource.getUserModel(id)); } // 再查是否有同userId的正在进行的请求 if (_pendingRequests.containsKey(id)) { return _pendingRequests[id]!; } // 发起新请求并存入pending缓存 final request = remoteDataSource.getUserInfo(id); _pendingRequests[id] = request; try { final result = await request; result.fold( (failure) => null, // 请求失败不写入缓存 (user) => localDataSource.cacheUserModel(user), ); return result; } finally { // 请求完成无论成功失败都移除pending缓存 _pendingRequests.remove(id); } } on ServerExceptions catch (e) { _pendingRequests.remove(id); return left(e.when(() => Failure(), requestCancelled: () => Failure.requestCancelled(), unauthorisedRequest: () => Failure.unauthorisedRequest(), badRequest: (e) => Failure.badRequest(e), notFound: () => Failure.notFound(), internalServerError: () => Failure.internalServerError(), receiveTimeout: () => Failure.receiveTimeout(), sendTimeout: () => Failure.sendTimeout(), noInternetConnection: () => Failure.noInternetConnection())); } on CacheException { _pendingRequests.remove(id); return left(Failure.cacheFailure()); } } }
2. 改造Bloc支持多用户状态存储
避免不同用户的加载状态互相覆盖,调整状态结构如下:
// UserInfoState定义 part of 'user_info_bloc.dart'; @freezed class UserInfoState with _$UserInfoState { const factory UserInfoState({ required Map<int, UserInfoLoadState> userStates, }) = _UserInfoState; factory UserInfoState.initial() => const UserInfoState(userStates: {}); } // 单个用户的加载状态 @freezed class UserInfoLoadState with _$UserInfoLoadState { const factory UserInfoLoadState.initial() = _Initial; const factory UserInfoLoadState.loading() = _Loading; const factory UserInfoLoadState.loaded(User user) = _Loaded; const factory UserInfoLoadState.error() = _Error; }
调整Bloc的事件处理逻辑:
on<UserInfoEvent>((event, emit) async { event.map( getUserInfo: (event) async { final userId = event.userId; // 已加载完成的用户无需重复处理 if (state.userStates[userId] is _Loaded) return; // 发射当前用户的loading状态 emit(state.copyWith( userStates: Map.from(state.userStates) ..[userId] = const UserInfoLoadState.loading(), )); final result = await userInfoRepository.getUserInfo(userId); result.fold( (failure) => emit(state.copyWith( userStates: Map.from(state.userStates) ..[userId] = const UserInfoLoadState.error(), )), (user) => emit(state.copyWith( userStates: Map.from(state.userStates) ..[userId] = UserInfoLoadState.loaded(user), )), ); }, ); });
注:UserInfoBloc需要注册为全局单例,确保全App共享同一个状态实例。
3. 优化Avatar组件的请求触发时机
将组件改为StatefulWidget,仅在初始化时触发一次请求,避免build重复触发请求,同时修复空指针风险:
class UserAvatar extends StatefulWidget { final int userId; final double radius; const UserAvatar({required this.userId, this.radius = 30, super.key}); @override State<UserAvatar> createState() => _UserAvatarState(); } class _UserAvatarState extends State<UserAvatar> { @override void initState() { super.initState(); context.read<UserInfoBloc>().add(UserInfoEvent.getUserInfo(widget.userId)); } @override Widget build(BuildContext context) { return BlocBuilder<UserInfoBloc, UserInfoState>( builder: (context, state) { final userState = state.userStates[widget.userId] ?? const UserInfoLoadState.initial(); return userState.when( initial: () => const Text('Initial'), loading: () => const Text('Loading'), loaded: (user) { return Container( child: Avatar( shape: AvatarShape.circle(widget.radius), loader: Center( child: ClipOval( child: Skeleton( height: widget.radius * 2, width: widget.radius * 2, ), ), ), useCache: true, sources: [NetworkSource(user.avatarUrl ?? '')], name: user.firstName?.trim() ?? '', onTap: () { //TODO implement Navigation to profile page }, ), ); }, error: () => const Text('error'), ); }, ); } }
内容的提问来源于stack exchange,提问作者Nebex Elias
相关产品推荐
相关产品推荐

