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

修改Stream Builder内的Future Builder以避免组件闪烁

解决StreamBuilder嵌套FutureBuilder导致UI闪烁的问题

一、可以在Activity流中一次性查询用户数据,移除嵌套的FutureBuilder

核心思路是通过switchMap将Activity的流转换为包含用户数据的组合数据流,一次性批量查询所有需要的用户,再关联到对应的Activity条目上,避免每个Item单独发起请求。

步骤1:定义组合数据模型

创建一个包含Activity和对应用户的模型,方便后续传递数据:

class ActivityWithUsers {
  final ActivityModel activity;
  final List<UserModel> users;

  ActivityWithUsers({required this.activity, required this.users});
}

步骤2:改造Activity流,合并用户数据

修改activityStream方法,先获取Activity快照,收集所有需要查询的用户UID(去重避免重复请求),批量查询用户后,将用户数据与对应的Activity关联:

Stream<List<ActivityWithUsers>> activityWithUsersStream() {
  return FirebaseFirestore.instance
      .collection('activity')
      .orderBy('timestamp', descending: true)
      .limit(55)
      .snapshots()
      .switchMap((activitySnapshot) async* {
    // 转换Activity数据
    final activities = activitySnapshot.docs
        .map((doc) => ActivityModel.fromFirestore(doc))
        .toList();

    // 收集所有需要的用户UID并去重
    final allUserUids = activities
        .expand((activity) => activity.players)
        .toSet()
        .toList();

    if (allUserUids.isEmpty) {
      // 没有用户需要查询,直接返回空用户列表的Activity
      yield activities
          .map((activity) => ActivityWithUsers(
                activity: activity,
                users: [],
              ))
          .toList();
      return;
    }

    // 批量查询用户数据
    final userSnapshot = await FirebaseFirestore.instance
        .collection('users')
        .where('uid', whereIn: allUserUids)
        .get();

    final users = userSnapshot.docs
        .map((doc) => UserModel.fromFirestore(doc))
        .toList();

    // 将用户数据关联到对应的Activity
    final activityWithUsersList = activities.map((activity) {
      final matchedUsers = users
          .where((user) => activity.players.contains(user.uid))
          .toList();
      return ActivityWithUsers(
        activity: activity,
        users: matchedUsers,
      );
    }).toList();

    yield activityWithUsersList;
  });
}

步骤3:重构UI层,移除嵌套的FutureBuilder

现在可以直接使用改造后的流,不需要每个Item单独发起请求:

StreamBuilder<List<ActivityWithUsers>>(
  stream: activityWithUsersStream(),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    switch (snapshot.connectionState) {
      case ConnectionState.waiting:
        return const Center(child: CircularProgressIndicator());
      default:
        final activityWithUsersList = snapshot.data ?? [];

        return Scrollbar(
          controller: widget.scrollController,
          child: ListView.builder(
            shrinkWrap: true,
            controller: widget.scrollController,
            itemCount: activityWithUsersList.length,
            itemBuilder: (context, i) {
              final item = activityWithUsersList[i];
              // 直接使用已关联的用户数据
              return MyWidget(users: item.users);
            },
          ),
        );
    }
  },
);

二、如果暂时无法改造数据流,可行的优化方案

1. 缓存用户查询结果,避免重复请求

每次查询用户后将结果缓存到内存或本地存储(比如Hive、SharedPreferences),下次需要时优先读取缓存,同时后台更新最新数据,等待时显示缓存内容而非空盒子:

// 内存缓存示例
final Map<String, UserModel> _userCache = {};

Future<List<UserModel>> getUsers(List<String> uids) async {
  // 先查缓存
  final cachedUsers = uids
      .map((uid) => _userCache[uid])
      .whereType<UserModel>()
      .toList();
  final missingUids = uids.where((uid) => !_userCache.containsKey(uid)).toList();

  if (missingUids.isEmpty) return cachedUsers;

  // 查询缺失的用户
  final snapshot = await FirebaseFirestore.instance
      .collection('users')
      .where('uid', whereIn: missingUids)
      .get();
  final newUsers = snapshot.docs.map((e) => UserModel.fromFirestore(e)).toList();
  
  // 更新缓存
  for (var user in newUsers) {
    _userCache[user.uid] = user;
  }

  return [...cachedUsers, ...newUsers];
}

2. 优化等待状态的UI,保持布局稳定

不要返回空的SizedBox,而是返回和MyWidget高度一致的骨架屏或加载状态,避免布局跳动:

if (snapshot.connectionState == ConnectionState.waiting) {
  return Container(
    height: 65.0,
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      children: [
        // 模拟头像骨架
        Container(
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            color: Colors.grey[200],
            borderRadius: BorderRadius.circular(20),
          ),
        ),
        SizedBox(width: 12),
        // 模拟文本骨架
        Expanded(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                height: 14,
                width: double.infinity,
                color: Colors.grey[200],
                margin: EdgeInsets.only(bottom: 6),
              ),
              Container(
                height: 12,
                width: 100,
                color: Colors.grey[200],
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

3. 避免FutureBuilder重复发起请求

当前代码中,itemBuilder每次重建都会创建新的Future,导致频繁重复请求。可以将Future缓存到ActivityModel中,或者使用StatefulWidget存储Future:

// 示例:在ActivityModel中缓存用户查询Future
class ActivityModel {
  // ...原有字段
  List<String> players;
  Future<List<UserModel>>? _userFuture;

  Future<List<UserModel>> getUsers() {
    _userFuture ??= FirebaseFirestore.instance
        .collection('users')
        .where('uid', whereIn: players)
        .get()
        .then((snapshot) => snapshot.docs.map((e) => UserModel.fromFirestore(e)).toList());
    return _userFuture!;
  }
}

// 然后在itemBuilder中使用
return FutureBuilder(
  future: activityContent[i].getUsers(),
  builder: (context, snapshot) {
    // ...原有逻辑
  },
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 11:05:20