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
相关产品推荐
相关产品推荐

