Search code examples
flutterfirebasegoogle-cloud-firestoreriverpod

Flutter Riverpod Streamprovider with Firebase Document Listener


I have written a riverpod stream provider in flutter, which should wait after successful authentication of the user until the user document exists. if this exists, the document should be read out and the data parsed. however, I have a problem with implementing a possibility that waits for the data. I have already tried it with a listener on the firebase document, but I can't get the data out of the listener and therefore can't return it. since I am not so experienced with such use cases, I don't know how I can prepare this data in such a way that I end up with the following facts.

when a user is authenticated, the process starts to wait for our user's data (already working). when the data is available, it should be parsed and returned from a riverpod StreamProvider. (I can no longer give out the most recent values).

When I Try to return my current user from inside the Firestore listener i get the vollowing error: The return type 'StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>' isn't a 'Stream', as required by the closure's context.

I attach my demo code here:

//this Provider contains the information for the auth state
final authProvider = StreamProvider<User?>((ref) {
  return FirebaseAuth.instance.authStateChanges();
});


//this Provider is responsible for loading the data as soon as possible
final currentUserProvider = StreamProvider<CurrentUser>((ref) {
  final authState = ref.watch(authProvider);

  return authState.when(
    data: (user) { //starts as soon as the user is authenticated

      String uid = user!.uid;
  
      // User is logged in, fetch user data from Firestore
      final firestore = FirebaseFirestore.instance;

      return firestore.collection('users').doc(uid).snapshots().listen((snapshot) {
        if(snapshot.exists){
          //I want to return my data from here
          return cu.fromJson(snapshot.data()!); //Parsed the Data from the snapshot
        } else {
          return Stream.empty();
          print("does not exist");
        }
      });

      //firestore.collection('users').doc("test").set({
      //  "name": "test"
      //}); //this can be used to create test data
    },
    loading: () => const Stream.empty(),
    error: (_, __) => const Stream.empty(),
  );
});

Solution

  • You shouldn't use .listen(...) if you want to return the stream itself. Use .map(...) to turn the Stream<DocumentSnapshot> to your CurrentUser stream.

    Here's the updated code:

    final currentUserProvider = StreamProvider<CurrentUser>((ref) {
      final authState = ref.watch(authProvider);
    
      return authState.when(
        data: (user) { //starts as soon as the user is authenticated
    
          String uid = user!.uid;
      
          // User is logged in, fetch user data from Firestore
          final firestore = FirebaseFirestore.instance;
    
          return firestore.collection('users').doc(uid).snapshots().map((snapshot) {
            if(snapshot.exists){
              return cu.fromJson(snapshot.data()!);
            } else {
              print("Does not exist");
              const Stream<CurrentUser>.empty(),
            }
          });
        },
        loading: () => const Stream<CurrentUser>.empty(),
        error: (_, __) => const Stream<CurrentUser>.empty(),
      );
    });
    

    Now for the second part - that you need to wait until the user document is created. You have the ability to refresh providers in Riverpod.

    So, right after the successful creation of the user document, you just have to do:

    ref.invalidate(currentUserProvider);
    

    Add this line where you actually create the user document. This line clears the current state of the currentUserProvider and immediately flushes it with new data from the stream.

    So, to handle the waiting time. You'll just have to show a temporary loading screen when the current user state is empty.

    Hope this helps!