1

Apologies for the lengthy post, alot of context and code is provided. Scroll down to TL;DR section for crux of the question.

Context:

My application needs to listen to any profile changes of a user, whose data is stored in Firebase's Firestore.

As described in multiple questions/posts found here, here, here, here and here (and many more). One should use the Flutter Way by making use of streams, etc.

Since I am interested in determining if the user is authenticated (if not goto Login, if yes, goto Home), I make use of 2 streams

  1. FirebaseAuth.instance.authStateChanges()

This is used to determine the current logged in state of the user were a StreamBuilder is used to determine if we should goto Login or goto Home

  1. FirebaseFirestore.instance.collection('users').doc(uid).snapshots().map((event) => ...)

This is used when a valid authentication state is established either by registering, logging in or opening the app.

This second stream is problematic


Problem:

When the first stream is triggered and returns a successful authentication i.e. FirebaseAuth.instance.currentUser != null, the 2nd stream is triggered with:

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

If I make any changes in the firestore emulator this doesn't trigger. If I login successfully, this does trigger but stops at the .snapshots() with no error message.

If I manually call this from a button click:

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

only the Stream.first successfully returns a value, calling Stream.toList or Stream.last or Stream.single, the execution just stops, no error, no crash, just stops but the app remains running w/o any changes.

According to the StreamBuilder documentation:

Creates a new StreamBuilder that builds itself based on the latest snapshot of interaction with the specified stream and whose build strategy is given by builder.

TL;DR

This indicates to me that the Stream.last execution may be problematic

For Example:

Manually invoking the following 2 set of code has different results. Both acquire the getCurrentUser(Uid) stream (see above class Firestore for impl). The stream has a Map<String, dynamic> to UserModel mapping function attached (see above). The mapping function prints out the content when it is mapped to the UserModel

This prints out the UserModel mapping AND then prints "Got user model" followed by the model.

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

This prints out the UserModel mapping BUT does not execute the .then(), or .catchError() or .whenComplete().

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

Thus, after calling currentUser.last (and mapping to UserModel), the execution stops, not even returning the model - just stops dead.

Am I doing something wrong?


Code & Implementation Details below:

Firestore Class retrieves a stream of the current user's document and should listen for changes:

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

UiRoot uses the authStateChanges() method to check for updated user auth states, then if they are auth'ed, builds the LoggedIn() widget

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;
    }
}

The LoggedIn widget is the problematic widget. The Stream consistently returns null. (See below for message output)

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 class

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.g helper

// 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),
    };

Log output (custom messages)

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]

I am expecting to see (from LoggedIn() widget):

I/flutter (17988): UserModel is NOT null

in place of:

I/flutter (17988): UserModel is null

Update:

Link to github project.

To get started:

  1. Throw in your own gooogle-services.json file into android/app folder
  2. change all references for com.flutterprojects.myapp to ${your google-services.json package name}
CybeX
  • 2,060
  • 3
  • 48
  • 115
  • what do you see on the logs if you call `Firestore().getCurrentUser(currentUid).listen(print)`? – pskink Jun 17 '21 at 07:40
  • @pskink the stream acknowledges the change. It prints out `Firestore change value: Instance of 'UserModel'` when I added the line `print("Firestore change value: $data");` to the `listen()` function. Making changes in the Firestore emulator call the listen function again acknowledging the update. – CybeX Jun 17 '21 at 13:28
  • @pskink what I find strange is: after registering, making any change on Firestore triggers the `listen()` stream, but the `StreamBuilder` doesn't budge, as if nothing happened at all. Second, when launching for the first time (after login), when the `StreamBuilder` returns the initial value i.e.`null` and stops – CybeX Jun 17 '21 at 13:40
  • its impossible: `StreamBuilder` simply calls `Stream.listen` and on every data that is passed to `listen`'s callback it simply calls `builder` callback - post your `StreamBuilder` code – pskink Jun 17 '21 at 13:51
  • 1
    You can get rid of your first stream. If you define the firestore rules to only allow authenticated users to read and write, you would just need one stream to get user details. Firebase automatically wont return you with user details hence you can put a check on the resoponse of second stream. – devDeejay Jun 17 '21 at 14:20
  • @pskink I posted the entire project (name changes, `google-services.json` omitted). You can find the `StreamBuilder` code as well as the `StreamBuilder` `Firebase().getCurrentUser(uid)` code (just below the **Problem** section) (search for `Stream getCurrentUser`) – CybeX Jun 17 '21 at 14:21

0 Answers0