基于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

