You need to enable JavaScript to run this app. 如何避免Flutter Bloc场景下相同用户信息重复发起网络请求?
优惠活动
大模型
产品
解决方案
定价
更多

如何避免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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.15 05:36:05