Flutter中如何全局存储多次复用的Firestore DocumentReference
数据库结构
我使用的Firestore数据库包含两个集合:users 和 groups。
每个用户都关联一个分组,应用会在多处拉取对应分组的DocumentReference,用于后续查询其他业务数据。
当前实现方案
我目前封装了供所有Firestore数据页面继承的StatefulWidget基类,实现代码如下。当前每个页面实例都会单独持有一份groupDocRef,重复初始化带来了不必要的开销,需要实现跨页面的引用复用。
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:shavtzak/data/soldier.dart'; import 'package:shavtzak/widgets/placeholder.dart'; import '../constants.dart'; import '../utils.dart'; /// 所有需要展示Firestore数据的页面的[StatefulWidget]基类 abstract class BaseFirestoreState<T extends StatefulWidget> extends State<T> { /// 当前[FirebaseAuth]登录用户 User? _user; StreamSubscription<User?>? _authStateChangeListener; DocumentReference<Map<String, dynamic>>? _nullableGroupDoc; /// 对应用户所属分组文档的[DocumentReference] late DocumentReference<Map<String, dynamic>> groupDocRef; @override @nonVirtual Widget build(BuildContext context) { // 开始监听用户登录状态变化 if (_authStateChangeListener == null) { _authStateChangeListener = FirebaseAuth.instance.authStateChanges().listen((User? user) { if (user != null) { _user = user; Utils.getGroupDoc(_user!).then((value) => {setState(() => _nullableGroupDoc = value)}); } }); } // 若分组文档引用为空,展示空状态占位页 if (_nullableGroupDoc == null) return NoDataPlaceholder(); // 否则加载子页面 else groupDocRef = _nullableGroupDoc!; return buildWithDoc(context); } /// 子页面实现的widget构建方法 /// /// 替代默认的[build]方法 Widget buildWithDoc(BuildContext context); /// 返回[Soldier]列表的[Stream] Stream<QuerySnapshot<Map<String, dynamic>>> getSoldiersStream() { return groupDocRef.collection(Constants.dbColSoldiers).snapshots(); } /// 从Firestore [QuerySnapshot]中解析并返回士兵列表 List<Soldier> soldierListFromSnapshot(QuerySnapshot<Map<String, dynamic>> snapshot) { return snapshot.docs.map((DocumentSnapshot<Map<String, dynamic>> doc) { Soldier? sol; // 无数据时返回null if (doc.data() == null) { sol = null; } // 解析数据返回对应的士兵实例 else { sol = Soldier.fromMap(doc.data()!, doc.id); } return sol!; }).toList(); } @override void dispose() { if (_authStateChangeListener != null) { _authStateChangeListener!.cancel(); } super.dispose(); } }
咨询问题
单次应用会话内重复读取同一个DocumentReference会产生额外性能开销,如何实现全局可访问的DocumentReference缓存,仅执行一次获取逻辑就能在全应用范围内复用?
实现方案
核心思路是把分组引用的初始化逻辑从各个页面抽离,做成全局单例缓存,只初始化一次,所有页面统一访问缓存实例即可。
第一步:创建全局Firestore服务单例
import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:shavtzak/constants.dart'; import 'package:shavtzak/data/soldier.dart'; import 'package:shavtzak/utils.dart'; class FirestoreService { // 单例构造 static final FirestoreService _instance = FirestoreService._internal(); factory FirestoreService() => _instance; FirestoreService._internal(); // 缓存的分组文档引用 DocumentReference<Map<String, dynamic>>? groupDocRef; StreamSubscription<User?>? _authStateSub; /// 应用启动时调用一次,完成全局初始化 void init() { // 全局只注册一次登录状态监听,避免每个页面重复注册 _authStateSub = FirebaseAuth.instance.authStateChanges().listen((user) async { if (user != null) { // 引用为空时才执行获取,不会重复调用接口 groupDocRef ??= await Utils.getGroupDoc(user); } else { // 用户退出登录时清空缓存,避免多账号数据串扰 groupDocRef = null; } }); } /// 统一提供士兵集合查询流 Stream<QuerySnapshot<Map<String, dynamic>>> getSoldiersStream() { assert(groupDocRef != null, '请完成登录初始化分组引用后再调用数据方法'); return groupDocRef!.collection(Constants.dbColSoldiers).snapshots(); } /// 统一提供快照转士兵列表的解析方法 List<Soldier> parseSoldierList(QuerySnapshot<Map<String, dynamic>> snapshot) { return snapshot.docs.map((doc) { final docData = doc.data(); if (docData == null) throw StateError('士兵文档数据为空'); return Soldier.fromMap(docData, doc.id); }).toList(); } /// 应用退出时调用释放资源 void dispose() { _authStateSub?.cancel(); } }
第二步:应用启动时初始化服务
在main函数中runApp之前调用一次初始化即可:
void main() { WidgetsFlutterBinding.ensureInitialized(); // 初始化全局Firebase配置... FirestoreService().init(); runApp(const MyApp()); }
第三步:简化原有页面基类
移除基类中重复的登录监听、引用缓存逻辑,直接访问全局服务即可:
abstract class BaseFirestoreState<T extends StatefulWidget> extends State<T> { @override @nonVirtual Widget build(BuildContext context) { final groupRef = FirestoreService().groupDocRef; // 全局引用未初始化完成时展示占位页 if (groupRef == null) return const NoDataPlaceholder(); return buildWithDoc(context); } Widget buildWithDoc(BuildContext context); Stream<QuerySnapshot<Map<String, dynamic>>> getSoldiersStream() { return FirestoreService().getSoldiersStream(); } List<Soldier> soldierListFromSnapshot(QuerySnapshot<Map<String, dynamic>> snapshot) { return FirestoreService().parseSoldierList(snapshot); } }
补充说明
DocumentReference本身只是指向文档位置的轻量对象,本身不持有数据,重复创建的开销极低,原有代码的性能损耗主要来自每个页面重复注册登录状态监听、重复调用Utils.getGroupDoc的逻辑,抽离全局服务后这部分重复开销会完全消除。- 如果项目中已经使用了Provider、Riverpod、GetIt这类状态管理/依赖注入框架,也可以直接把这个服务放到全局注入容器中管理,核心缓存逻辑和单例实现完全一致。
- 额外增加了退出登录清空缓存的逻辑,避免切换账号时读取到上一个用户的分组数据。
内容的提问来源于stack exchange,提问作者Ido Fang Bentov
相关产品推荐
相关产品推荐

