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

