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

Flutter Firestore通过StreamBuilder监听单文档snapshots流不更新问题

抱歉帖子内容较长,提供了大量上下文和代码,可滚动到TL;DR部分查看问题核心。

背景

我的应用需要监听存储在Firebase Firestore中的用户个人资料的所有变更。
根据多个公开的技术帖子和官方教程所述,应当遵循Flutter官方推荐的实现方式,使用Stream等能力完成需求。
由于我需要判断用户是否已完成认证(未认证跳转登录页,已认证跳转首页),因此我使用了2个Stream:

  • FirebaseAuth.instance.authStateChanges():该Stream用于判断用户当前的登录状态,搭配StreamBuilder决定跳转登录页还是首页。
  • FirebaseFirestore.instance.collection('users').doc(uid).snapshots().map((event) => ...):该Stream在用户完成注册、登录或打开应用,确认处于有效认证状态后使用。

目前第二个Stream存在异常。


问题描述

当第一个Stream触发并返回认证成功结果,即FirebaseAuth.instance.currentUser != null时,第二个Stream会被触发,对应实现代码如下:

class Firestore {
  Stream<UserModel?> getCurrentUser(String uid) {
    return FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .snapshots()
        .map((event) => UserModel.fromJson(event.data() ?? {}));
  }
}

如果我在Firestore模拟器中修改数据,该Stream不会触发。如果我成功登录,Stream会触发但执行到.snapshots()时就卡住,无任何错误信息输出。

如果我通过按钮点击手动调用如下代码:

var currentUser = Firestore().getCurrentUser(FirebaseAuth.instance.currentUser!.uid);
var first = await currentUser.last;

只有Stream.first可以成功返回值,调用Stream.toList、Stream.last或Stream.single时,执行直接停止,无错误、无崩溃,仅执行卡住,但应用仍正常运行无异常提示。

根据StreamBuilder官方文档说明:

创建一个新的StreamBuilder,它会基于与指定Stream交互的最新快照进行构建,构建逻辑由builder参数定义。

TL;DR

以上现象说明Stream.last的执行可能存在问题。

举例说明

手动调用以下两组代码会得到不同结果,两者都调用getCurrentUser(Uid)获取Stream(实现见上方Firestore类),该Stream附带了Map<String, dynamic>转UserModel的映射函数,映射函数在将数据转换为UserModel时会打印对应内容。

第一组代码会打印UserModel映射结果,然后打印"Got user model"和对应的模型数据:

currentUser.first.then((value) {
  print("Got user model");
  print(value!.toJson());
});

第二组代码会打印UserModel映射结果,但不会执行.then()、.catchError()或.whenComplete():

currentUser.last.then((value) {
  print("Got user model");
  print(value!.toJson());
}).catchError((error) {
  print(error);
}).whenComplete(() {
  print("complete");
});

因此调用currentUser.last(并映射为UserModel)后执行直接卡住,甚至不会返回模型,完全停止运行。请问我是不是哪里实现有误?


代码与实现细节

Firestore类

用于获取当前用户文档的Stream,支持监听数据变更:

class Firestore {
  Stream<UserModel?> getCurrentUser(String uid) {
    return FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .snapshots()
        .map((event) => UserModel.fromJson(event.data() ?? {}));
  }
}

UiRoot组件

使用authStateChanges()方法监听用户认证状态更新,如果用户已认证则构建LoggedIn()组件:

class UiRoot extends StatefulWidget {
    @override
    _UiRootState createState() => new _UiRootState();
}

class _UiRootState extends State<UiRoot> {

    Widget build(BuildContext context) {
      Widget retVal;
  
      switch (_authStatus) {
          case AuthStatus.notLoggedIn:
              ...
              break;
          case AuthStatus.loggedIn:
              print("checking logged in");
              retVal = StreamProvider<UserModel?>.value(
                  value: Firestore().getCurrentUser(currentUid),
                  initialData: null,
                  child: LoggedIn(),
              );
              break;
          case AuthStatus.unknown:
          default:
              ...
              break;
      }
      return retVal;
    }
}

LoggedIn组件

是出现异常的组件,Stream始终返回null(输出日志见下文):

class LoggedIn extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return StreamBuilder<UserModel?>(
      builder: (context, snapshot) {
        print("Checking UserModel from stream provider");
        if (snapshot.data == null) {
          print("UserModel is null");
          return UiLogin();
        }

        print("UserModel is NOT null");
        return UiHome();
      },
    );
  }
}

UserModel类代码

part 'user_model.g.dart';

@JsonSerializable(explicitToJson: true)
class UserModel extends Object {

  final String uid;
  final String name;
  final String email;
  final String bio;
  final String fcmToken;
  @JsonKey(fromJson: _dateTimeFromMilliseconds, toJson: _dateTimeToMilliseconds)
  final DateTime createdDate;

  UserModel({required this.uid, required this.name, required this.email, required this.createdDate, required this.bio, required this.fcmToken});

  factory UserModel.fromJson(Map<String, dynamic> json) =>
      _$UserModelFromJson(json);
  
  Map<String, dynamic> toJson() => _$UserModelToJson(this);

  factory UserModel.fromDocumentSnapshot(
      {required DocumentSnapshot<Map<String, dynamic>> doc}) =>
      UserModel.fromJson(doc.data() ?? {});

  static DateTime _dateTimeFromMilliseconds(String? timeformat) => timeformat == null ? DateTime.now() : DateTime.parse(timeformat);

  static String _dateTimeToMilliseconds(DateTime? dateTime) => dateTime == null ? DateTime.now().toIso8601String() : dateTime.toIso8601String();

  static fromFirebaseUser({required User user}) {
    return Firestore().getCurrentUser(user.uid);
  }

UserModel自动生成辅助代码

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user_model.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

UserModel _$UserModelFromJson(Map<String, dynamic> json) {
  print("Mapping json to object");
  if (json == null || json.length == 0) {
    return UserModel(uid: "", name: "", email: "", createdDate: DateTime.now(), bio: "", fcmToken: "");
  }
  print(json.entries.map((e) => e.key + e.value).fold("", (previousValue, element) => "$previousValue,\n$element"));
  return UserModel(
    uid: json['uid'] as String,
    name: json['name'] as String,
    email: json['email'] as String,
    createdDate:
        UserModel._dateTimeFromMilliseconds(json['createdDate'] as String?),
    bio: json['bio'] as String,
    fcmToken: json['fcmToken'] as String,
  );
}

Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
      'uid': instance.uid,
      'name': instance.name,
      'email': instance.email,
      'bio': instance.bio,
      'fcmToken': instance.fcmToken,
      'createdDate': UserModel._dateTimeToMilliseconds(instance.createdDate),
    };
日志输出

自定义消息日志如下:

I/flutter (17988): AuthModel state changed
I/flutter (17988): AuthModel logged in state
I/flutter (17988): checking logged in
I/flutter (17988): Checking UserModel from stream provider
I/flutter (17988): UserModel is null
I/flutter (17988): registered successfully with uid [82e825c1-4437-4939-8a37-e6610e84b3a1]

我期望从LoggedIn()组件看到如下输出:

I/flutter (17988): UserModel is NOT null

而非实际输出的:

I/flutter (17988): UserModel is null

问题解答

1. Stream.last 卡住的原因

Firestore的snapshots()方法返回的是无限持续的广播流:只要对应文档有变更就会持续下发新快照,除非你手动取消监听,否则流永远不会触发完成事件。
Stream.last的实现逻辑是等待流触发完成事件后,返回最后一次下发的值,所以调用它会永远处于等待状态,这是预期行为,不是实现错误。你可以使用first获取最新的一次快照,或者手动给Stream加take(1)操作符限定只取一次数据,之后流会自动结束,此时调用last也能正常返回。

2. LoggedIn组件始终返回null的原因

你代码里有两个明显的实现错误:

  • 你已经在父组件通过StreamProvider提供了UserModel?类型的流,子组件LoggedIn里的StreamBuilder没有指定stream参数,它只会使用默认初始值null,永远不会响应流的更新。
    你可以直接删除这个多余的StreamBuilder,用如下代码读取Provider的值即可:
class LoggedIn extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    final userModel = context.watch<UserModel?>();
    print("Checking UserModel from stream provider");
    if (userModel == null) {
      print("UserModel is null");
      return UiLogin();
    }
    print("UserModel is NOT null");
    return UiHome();
  }
}
  • 你的代码没有给流添加错误处理逻辑,如果Firestore安全规则拒绝了读请求、或者用户文档不存在,流会抛出异常,你看不到报错自然无法定位问题。你可以在map操作符前添加handleError打印错误:
Stream<UserModel?> getCurrentUser(String uid) {
  return FirebaseFirestore.instance
      .collection('users')
      .doc(uid)
      .snapshots()
      .handleError((e) => print("Firestore读错误: $e"))
      .map((event) => UserModel.fromJson(event.data() ?? {}));
}

另外建议检查你的Firestore安全规则,确保登录用户有权限读取自己的users文档,示例规则如下:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow read, write: if request.auth != null && request.auth.uid == uid;
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.16 06:45:03