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

Flutter Firebase Cloud Firestore:筛选含指定用户点赞子集合的食谱流

解决Firestore获取用户收藏食谱并按收藏顺序排序的问题

我来帮你搞定这个需求!你之前的查询写法不对,而且Firestore本身不支持直接跨集合查询子集合的条件来获取父文档。下面给你两种可行的方案,推荐第一种,因为它更符合Firestore的非规范化设计思路,能轻松实现按收藏顺序查看的核心需求:

方案一:反向存储用户收藏记录(推荐)

Firestore的最佳实践是通过非规范化数据来优化查询,尤其是这种需要按关联顺序排序的场景。我们可以在用户文档下添加一个likedRecipes子集合,专门存储用户收藏的食谱ID和对应的收藏日期,这样查询起来会非常高效。

第一步:更新点赞逻辑(收藏时同步数据)

当用户点赞某个食谱时,我们需要同时在两个地方写入数据:

  1. 原食谱的likedBy子集合(保持原来的结构,方便统计点赞数等)
  2. 用户专属的likedRecipes子集合(用于快速查询收藏列表)
Future<void> likeRecipe(String userId, String recipeId) async {
  final batch = FirebaseFirestore.instance.batch();
  
  // 1. 在目标食谱的likedBy子集合添加用户收藏记录(带时间戳)
  batch.set(
    FirebaseFirestore.instance
        .collection('recipes')
        .doc(recipeId)
        .collection('likedBy')
        .doc(userId),
    {'likedDate': FieldValue.serverTimestamp()}
  );
  
  // 2. 在用户的likedRecipes子集合添加收藏记录(带时间戳)
  batch.set(
    FirebaseFirestore.instance
        .collection('users')
        .doc(userId)
        .collection('likedRecipes')
        .doc(recipeId),
    {'likedDate': FieldValue.serverTimestamp()}
  );
  
  await batch.commit();
}

第二步:查询用户收藏的食谱(按收藏顺序)

先查询用户的likedRecipes集合(按收藏日期降序,最新收藏的在前),再批量获取对应的食谱数据,并保持顺序:

String userId = 'uiuu4fn3fff4fu';

Scaffold(
  body: StreamBuilder<QuerySnapshot>(
    // 先获取用户的收藏记录,按收藏日期倒序排列
    stream: FirebaseFirestore.instance
        .collection('users')
        .doc(userId)
        .collection('likedRecipes')
        .orderBy('likedDate', descending: true)
        .snapshots(),
    builder: (context, snapshot) {
      // 处理加载状态
      if (snapshot.connectionState == ConnectionState.waiting) {
        return const Center(child: CircularProgressIndicator());
      }
      if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
        return const Center(child: Text('还没有收藏任何食谱'));
      }
      
      // 提取所有收藏的食谱ID
      final recipeIds = snapshot.data!.docs.map((doc) => doc.id).toList();
      
      // 批量获取对应的食谱数据
      return StreamBuilder<QuerySnapshot>(
        stream: FirebaseFirestore.instance
            .collection('recipes')
            .where(FieldPath.documentId, whereIn: recipeIds)
            .snapshots(),
        builder: (context, recipeSnapshot) {
          if (recipeSnapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (!recipeSnapshot.hasData) {
            return const Center(child: Text('加载失败'));
          }
          
          // 把食谱数据映射成ID-文档的字典,方便后续按收藏顺序排序
          final recipeMap = {
            for (var doc in recipeSnapshot.data!.docs) doc.id: doc,
          };
          // 按照用户收藏的顺序重新排列食谱列表
          final orderedRecipes = recipeIds
              .map((id) => recipeMap[id])
              .whereType<DocumentSnapshot>()
              .toList();
          
          // 渲染收藏列表
          return ListView.builder(
            itemCount: orderedRecipes.length,
            itemBuilder: (context, index) {
              final recipe = orderedRecipes[index];
              return Padding(
                padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      recipe['recipeName'],
                      style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 4),
                    Text('准备时长: ${recipe['recipeDuration']}'),
                  ],
                ),
              );
            },
          );
        },
      );
    },
  ),
);

方案优势

  • 高效:直接查询用户专属的收藏集合,避免大范围的跨集合扫描
  • 易排序:天然支持按收藏日期排序,完全符合你要的Instagram式收藏体验
  • 实时更新:通过StreamBuilder可以实时同步收藏状态的变化

方案二:集合组查询(适合不想修改现有数据结构的情况)

如果你不想调整现有数据结构,可以用Firestore的集合组查询来找到所有包含该用户ID的likedBy子集合文档,再反向获取对应的食谱数据。但这个方案有一些限制:

第一步:创建集合组索引

你需要先在Firestore控制台创建一个集合组索引:

  1. 进入Firestore控制台 → 索引 → 集合组索引
  2. 创建新索引:
    • 集合ID:likedBy
    • 查询范围:集合组
    • 添加字段:__name__(文档ID)等于指定值,同时按likedDate排序

第二步:编写查询代码

String userId = 'uiuu4fn3fff4fu';

Scaffold(
  body: StreamBuilder<QuerySnapshot>(
    // 集合组查询所有likedBy子集合中ID为当前用户的文档
    stream: FirebaseFirestore.instance
        .collectionGroup('likedBy')
        .where(FieldPath.documentId, isEqualTo: userId)
        .orderBy('likedDate', descending: true)
        .snapshots(),
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        return const Center(child: CircularProgressIndicator());
      }
      if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
        return const Center(child: Text('还没有收藏任何食谱'));
      }
      
      // 获取每个收藏记录对应的父食谱文档引用
      final recipeRefs = snapshot.data!.docs
          .map((doc) => doc.reference.parent.parent!) // 从likedBy -> recipe文档
          .toList();
      
      // 批量获取所有食谱数据
      return FutureBuilder<List<DocumentSnapshot>>(
        future: Future.wait(recipeRefs.map((ref) => ref.get())),
        builder: (context, recipeSnapshot) {
          if (recipeSnapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (!recipeSnapshot.hasData) {
            return const Center(child: Text('加载失败'));
          }
          
          // 这里的顺序和集合组查询的顺序一致,因为Future.wait会按传入顺序返回结果
          final recipes = recipeSnapshot.data!;
          
          return ListView.builder(
            itemCount: recipes.length,
            itemBuilder: (context, index) {
              final recipe = recipes[index];
              return Padding(
                padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      recipe['recipeName'],
                      style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 4),
                    Text('准备时长: ${recipe['recipeDuration']}'),
                  ],
                ),
              );
            },
          );
        },
      );
    },
  ),
);

方案限制

  • 性能:集合组查询会扫描所有likedBy子集合,数据量大时性能不如方案一
  • 实时性:Future.wait是一次性获取数据,若要实时更新需要额外处理流的合并
  • 索引依赖:必须手动创建集合组索引,否则查询会失败

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 23:02:29