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

Flutter中如何通过StreamBuilder获取Firestore引用字段值并展示

问题原因

你现在的代码有三个核心错误,导致拿不到关联的用户名:

  • 把存储用户映射的UserSnapshotMap定义在build方法里,每次组件重绘这个Map都会被重置为空,根本存不住拉到的用户数据。
  • 全量监听users集合的所有文档性能极差,用户量上来之后会产生大量不必要的流量消耗,还可能触发Firestore的权限问题。
  • Reference类型字段的取值逻辑完全错误:Firestore返回的uid字段是DocumentReference类型对象,不是嵌套的用户数据Map,你写的documentSnapshot['uid']['fullname']属于取值路径错误,永远拿不到结果,还会报类型异常。
  • 两层嵌套StreamBuilder的写法会导致无意义的整页重绘,任意一层流触发更新都会刷新整个页面,性能很差。
正确实现方案

优先选择按需读取关联数据的方案,不要全量拉取users集合,把用户数据读取下沉到列表项层级,同时加内存缓存避免重复请求。

核心取值逻辑

Firestore的Reference类型字段读取后是DocumentReference实例,可直接通过该实例获取对应用户文档,不需要手动拼接路径:

// 从订单数据中拿到关联用户的引用
DocumentReference userRef = documentSnapshot['uid'] as DocumentReference;
// 单次读取用户数据
DocumentSnapshot userSnap = await userRef.get();
// 实时监听用户数据变化
Stream<DocumentSnapshot> userStream = userRef.snapshots();

修正后完整代码

将页面改为StatefulWidget,把缓存放到State中避免重绘重置:

class OrderListPage extends StatefulWidget {
  const OrderListPage({super.key});

  @override
  State<OrderListPage> createState() => _OrderListPageState();
}

class _OrderListPageState extends State<OrderListPage> {
  // 用户数据缓存,存在State中不会因重绘丢失
  final Map<String, Map<String, dynamic>> _userCache = {};

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
        // 仅监听当前司机的订单流,移除冗余的全量users监听
        stream: _db.collection('orders').where('driverId', isEqualTo: sp.getString('uid')).snapshots(),
        builder: (context, orderSnapshot) {
          if (!orderSnapshot.hasData) return const Center(child: Text('Loading...'));
          if (orderSnapshot.hasError) return const Center(child: Text('加载失败'));

          return ListView.builder(
            padding: const EdgeInsets.symmetric(vertical: 20),
            itemCount: orderSnapshot.data!.docs.length,
            itemBuilder: (context, index) {
              final orderDoc = orderSnapshot.data!.docs[index];
              final orderData = orderDoc.data();
              // 提取关联用户引用
              final DocumentReference userRef = orderData['uid'] as DocumentReference;
              final String userId = userRef.id;

              return Padding(
                padding: const EdgeInsets.symmetric(vertical: 20),
                child: Container(
                  width: MediaQuery.of(context).size.width,
                  height: 260,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(8),
                      border: Border.all(color: Colors.black38, width: 1)),
                  child: Column(
                    children: [
                      // 原有店铺信息模块保持不变
                      Row(
                        children: [
                          Padding(
                            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
                            child: Container(
                                height: 60,
                                width: 60,
                                decoration: BoxDecoration(
                                    borderRadius: BorderRadius.circular(6),
                                    border: Border.all(color: Colors.black12)),
                                child: Image.network(
                                    'https://indiaeducationdiary.in/wp-content/uploads/2020/10/IMG-20201024-WA0014.jpg')),
                          ),
                          Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                'Paradise Multicuisine Restaurant',
                                style: GoogleFonts.poppins(
                                    fontWeight: FontWeight.bold, fontSize: 14),
                                overflow: TextOverflow.ellipsis,
                              ),
                              Text(
                                '372,Kamarajar Salai, Karaikal',
                                style: GoogleFonts.poppins(
                                    fontWeight: FontWeight.w400,
                                    fontSize: 11,
                                    color: hexStringToColor('636567')),
                                overflow: TextOverflow.ellipsis,
                              ),
                            ],
                          ),
                        ],
                      ),
                      const DottedLine(
                        dashColor: Colors.black26,
                        dashGapLength: 10,
                      ),
                      Padding(
                        padding: const EdgeInsets.all(20),
                        child: SizedBox(
                          width: MediaQuery.of(context).size.height,
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Row(
                                children: [
                                  const Icon(Icons.account_circle),
                                  const SizedBox(width: 7,),
                                  // 按需读取/读取缓存用户数据
                                  FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>(
                                    future: _userCache.containsKey(userId)
                                        ? Future.value(_FakeDocumentSnapshot(userId, _userCache[userId]!))
                                        : userRef.get().then((snap) {
                                            if (snap.exists) {
                                              _userCache[userId] = snap.data()!;
                                            }
                                            return snap;
                                          }),
                                    builder: (context, userSnap) {
                                      String userName = 'Deleted User';
                                      if (userSnap.hasData && userSnap.data!.exists) {
                                        userName = userSnap.data!.data()!['fullname'] ?? 'Deleted User';
                                      }
                                      return Text(
                                        userName,
                                        style: GoogleFonts.poppins(
                                            fontSize: 12,
                                            fontWeight: FontWeight.w500,
                                            color: hexStringToColor('636567')),
                                      );
                                    },
                                  ),
                                  const SizedBox(width: 3),
                                  Text(
                                    '#${orderData['orderId']}',
                                    style: GoogleFonts.poppins(
                                        fontSize: 12,
                                        fontWeight: FontWeight.w500,
                                        color: hexStringToColor('636567')),
                                  )
                                ],
                              ),
                              // 原有地址、金额、按钮模块保持不变
                              SizedBox(
                                width: MediaQuery.of(context).size.width * 0.90,
                                child: Row(
                                  children: [
                                    const Icon(Icons.location_on_rounded),
                                    const SizedBox(width: 7,),
                                    Expanded(
                                      child: Text(
                                        '6/8, RR Colony, Ambedkar Street, Karaikal',
                                        style: GoogleFonts.poppins(
                                            fontSize: 12,
                                            fontWeight: FontWeight.w500,
                                            color: hexStringToColor('636567')),
                                        overflow: TextOverflow.clip,
                                        maxLines: 4,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: 20,),
                              Row(
                                children: [
                                  Container(
                                    decoration: BoxDecoration(
                                        color: hexStringToColor('aeaeae'),
                                        borderRadius: BorderRadius.circular(5)
                                    ),
                                    width: 80,
                                    height: 20,
                                    child: Row (
                                      mainAxisAlignment: MainAxisAlignment.center,
                                      children: [
                                        Text('3 Items',
                                          style: GoogleFonts.poppins(fontSize: 10, fontWeight: FontWeight.w500),
                                        )
                                      ],
                                    ),
                                  ),
                                  const SizedBox(width: 20,),
                                  Container(
                                    decoration: BoxDecoration(
                                        color: hexStringToColor('aeaeae'),
                                        borderRadius: BorderRadius.circular(5)
                                    ),
                                    width: 80,
                                    height: 20,
                                    child: Row (
                                      mainAxisAlignment: MainAxisAlignment.center,
                                      children: [
                                        Text("₹${orderData['grandTotal'].toString()}",
                                          style: GoogleFonts.poppins(fontSize: 10, fontWeight: FontWeight.w500),
                                        )
                                      ],
                                    ),
                                  ),
                                ],
                              ),
                              Align(
                                child: ElevatedButton(onPressed: (){},
                                  child: const Text('View Details'),
                                  style: ElevatedButton.styleFrom(
                                      elevation: 3,
                                      shape: RoundedRectangleBorder(
                                          borderRadius: BorderRadius.circular(5)
                                      ),
                                      textStyle: GoogleFonts.poppins(fontWeight: FontWeight.w400,fontSize: 14)
                                  ),
                                ),
                                alignment: Alignment.centerRight,
                              )
                            ],
                          ),
                        ),
                      )
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

// 用于返回缓存数据的简易DocumentSnapshot实现
class _FakeDocumentSnapshot implements DocumentSnapshot<Map<String, dynamic>> {
  final String _id;
  final Map<String, dynamic> _data;
  _FakeDocumentSnapshot(this._id, this._data);

  @override
  Map<String, dynamic> data() => _data;

  @override
  String get id => _id;

  @override
  bool get exists => true;

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

注意事项

  • 如果需要实时同步用户信息变化,把列表项中的FutureBuilder换成StreamBuilder,流参数传入userRef.snapshots()即可,缓存逻辑依然有效。
  • 禁止在build方法中定义需要持久化存储的变量,build方法会在每次组件重绘时全量执行,内部定义的变量都会被重新初始化。
  • 缓存方案会自动复用已经拉取过的用户数据,同一个用户关联多个订单时只会发起一次请求,不会重复消耗流量,也能大幅降低Firestore的使用成本。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 00:15:54