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

基于Flutter和Firestore的类Tinder匹配应用:如何实现用户仅单次出现在推荐流中

基于Flutter和Firestore的类Tinder匹配应用:如何实现用户仅单次出现在推荐流中

我之前做类Tinder的匹配应用时,也碰到过一模一样的问题——用户关闭重开App后又刷到已经滑过的人,用whereNotIn又受限于10个元素的上限,本地过滤又太费读取量。下面给你几个我试过或者验证过的可行方案,你可以根据自己的场景选:

方案1:优化版的「查询+本地过滤」,减少无效读取

既然whereNotIn有上限,那我们可以换个思路:每次从Firestore多请求几个用户(比如比需要的limit多10个),然后在本地过滤掉已经滑过的,最后凑够limit数量的结果返回。同时分页的时候,如果一次查询过滤后不够,就自动发起下一次查询,直到满足数量或者没有更多数据。

这种方式比直接请求200个再过滤要高效得多,因为我们只多请求少量数据来弥补过滤掉的部分,不会浪费太多读取量。

代码示例大概是这样:

Future<FirebasePaginatedResult<UserEntity>> getFeed({
  int limit = 20,
  DocumentSnapshot? lastDocument,
}) async {
  // 先获取本地或Firestore中存储的已滑用户ID列表
  final swipedIds = await _getSwipedUserIds(); 
  List<UserEntity> filteredUsers = [];
  DocumentSnapshot? currentLastDoc = lastDocument;
  bool hasMore = true;

  // 循环查询直到凑够需要的用户数,或者没有更多数据
  while (filteredUsers.length < limit && hasMore) {
    // 计算这次需要请求的数量:还缺的数量 + 额外10个用来过滤
    int fetchLimit = limit - filteredUsers.length + 10;
    var query = FirebaseFirestore.instance.collection('Users')
        .withConverter<UserEntity>(
          fromFirestore: (snapshot, _) => UserEntity.fromDoc(snapshot)!,
          toFirestore: (data, _) => data.toJson(),
        )
        .limit(fetchLimit);

    if (currentLastDoc != null) {
      query = query.startAfterDocument(currentLastDoc);
    }

    var snapshot = await query.get();
    // 判断是否还有更多数据
    hasMore = snapshot.docs.length == fetchLimit;
    currentLastDoc = snapshot.docs.lastOrNull;

    // 过滤掉已滑过的用户
    var batchUsers = snapshot.docs
        .map((e) => e.data())
        .where((user) => !swipedIds.contains(user.uid))
        .toList();

    filteredUsers.addAll(batchUsers);
  }

  // 只返回需要的limit数量的用户
  final resultUsers = filteredUsers.take(limit).toList();
  return FirebasePaginatedResult(
    data: resultUsers,
    hasMore: hasMore || filteredUsers.length > limit,
    lastDocument: currentLastDoc,
  );
}

方案2:分块处理已滑ID,绕过whereNotIn的10元素限制

如果你的用户滑过的人特别多,而且你更倾向于在Firestore层面就过滤掉已滑用户,可以把已滑的UID分成每10个一组,针对每组发起whereNotIn查询,最后合并结果并去重。

不过这个方案要注意:分块越多,发起的查询次数就越多,所以要权衡好。代码示例如下:

Future<FirebasePaginatedResult<UserEntity>> getFeed({
  int limit = 20,
  DocumentSnapshot? lastDocument,
}) async {
  final swipedIds = await _getSwipedUserIds();
  final List<List<String>> chunks = [];
  // 把已滑ID拆成每10个一组
  for (int i = 0; i < swipedIds.length; i += 10) {
    chunks.add(swipedIds.sublist(i, min(i + 10, swipedIds.length)));
  }

  List<UserEntity> allUsers = [];
  DocumentSnapshot? currentLastDoc = lastDocument;
  bool hasMore = true;

  while (allUsers.length < limit && hasMore) {
    // 多请求一些用户,应对分块过滤后的重复和损耗
    var baseQuery = FirebaseFirestore.instance.collection('Users')
        .withConverter<UserEntity>(
          fromFirestore: (snapshot, _) => UserEntity.fromDoc(snapshot)!,
          toFirestore: (data, _) => data.toJson(),
        )
        .limit(limit + chunks.length * 2);

    if (currentLastDoc != null) {
      baseQuery = baseQuery.startAfterDocument(currentLastDoc);
    }

    // 给每个分块创建查询任务
    List<Future<QuerySnapshot<UserEntity>>> queryFutures = [];
    for (var chunk in chunks) {
      queryFutures.add(baseQuery.where('uid', whereNotIn: chunk).get());
    }

    // 如果没有已滑过的用户,直接发起基础查询
    if (chunks.isEmpty) {
      queryFutures.add(baseQuery.get());
    }

    // 等待所有查询完成
    final snapshots = await Future.wait(queryFutures);
    final Set<String> seenUids = {}; // 用来去重
    for (var snapshot in snapshots) {
      hasMore = hasMore || snapshot.docs.length == baseQuery.limit;
      currentLastDoc = snapshot.docs.lastOrNull ?? currentLastDoc;
      for (var doc in snapshot.docs) {
        final user = doc.data();
        // 确保用户没被滑过,且没被添加过
        if (!seenUids.contains(user.uid) && !swipedIds.contains(user.uid)) {
          seenUids.add(user.uid);
          allUsers.add(user);
        }
      }
    }
  }

  final resultUsers = allUsers.take(limit).toList();
  return FirebasePaginatedResult(
    data: resultUsers,
    hasMore: hasMore || allUsers.length > limit,
    lastDocument: currentLastDoc,
  );
}

方案3:本地缓存+实时更新,进一步减少Firestore读取

如果用户频繁打开关闭App,你可以把未滑过的用户缓存到本地(比如用Hive、SharedPreferences或者Flutter的shared_preferences)。首次加载时先从缓存取,缓存不足时再从Firestore拉新的,同时过滤已滑用户。这样可以大幅减少Firestore的读取次数,提升用户打开App的速度。

总结

我个人更推荐方案1,它实现简单,在大多数场景下都能平衡性能和开发成本。如果你的用户量极大、滑过的用户数特别多,可以结合方案3的本地缓存来进一步优化。

备注:内容来源于stack exchange,提问作者Apri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.20 06:22:59