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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 00:57:22