6

I have been attempting to create streams to Firestore documents using the uid obtained from my auth Provider:

class AuthService {
  ...

  static final provider = StreamProvider.autoDispose((ref) => FirebaseAuth.instance.onAuthStateChanged);
  ...
}

However, I am struggling to actually create a StreamProvider dependent on the value from the auth Provider.

class User {
  ...
  static final provider = StreamProvider((ref) {
    final stream = ref.read(AuthService.provider);
    // Returns AsyncValue<Stream<User>> instead of desired AsyncValue<User>
    return stream.map((auth) => Service.user.stream(auth.uid));
  });
  ...
}

I also tried using Computed to return the uid or the stream itself but you cannot read a Computed from a Provider (which makes sense in retrospect).

This question is the most relevant on this topic but it is dealing with Provider, not Riverpod.

P.S. Can a Riverpod tag be created?

Edit:

The answer isn't working quite right. The await for loop is only ever triggering once, whereas a listener catches all events.

static final provider = StreamProvider((ref) async* {
  final stream = ref.read(AuthService.provider);
  print('userProvider init');

  stream.listen((auth) {
    print('LISTENED: ${auth?.uid}');
  });

  await for (final auth in stream) {
    print('uid: ${auth?.uid}');
    yield* Service.user.stream(auth?.uid);
  }
});

This code yields the following on login:

userProvider init
LISTENED: <redacted UID>
uid: <redacted UID>

And then on logout:

LISTENED: null

Where I would expect to see uid: null as well, which would update the stream, but upon any more auth events, only the listener is triggered and no events are caught by the await for loop.

Interestingly, using the flutter inspector, the value emitted by the auth provider never changes, either:

AutoDisposeStreamProvider<FirebaseUser>#95f11: AsyncValue<FirebaseUser>.data(value: FirebaseUser(Instance of 'PlatformUser'))

persists through login/logout events, which could explain this behavior, but I am not sure what to do to fix it.

Any ideas? I have been stuck on this for a while and can't correct the issue.

Delgan
  • 18,571
  • 11
  • 90
  • 141
Alex Hartford
  • 5,110
  • 2
  • 19
  • 36
  • I've updated the syntax to 0.6.0-dev – Rémi Rousselet Aug 03 '20 at 17:11
  • @RémiRousselet Awesome, thanks! I have been following your work on Riverpod and was excited to see that my problem would be fixed. I will accept your answer once 0.6.0 is released and I can test the changes. – Alex Hartford Aug 04 '20 at 19:47
  • The Riverpod docs say NOT to use "ref.read(" inside of a Provider constructor ... but I see Remi has reviewed this question & did not object to your code. I've got a pending issue on github looking for clarity on this point. – Dewey Nov 30 '20 at 04:50
  • 2
    @Dewey This question came before that change was made to use ref.watch instead. So, you are correct. – Alex Hartford Nov 30 '20 at 17:22

2 Answers2

10

The problem is, your provider doesn't create a Stream<User> but a Stream<Stream<User>>

As part of 0.6.0-dev, you can use ref.watch to easily combine streams:

class User {
  ...
  static final provider = StreamProvider((ref) {
    final auth = ref.watch(AuthService.provider);
    return Service.user.stream(auth.uid);
  });
  ...
}
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • That's some really nice syntax for that, I have some reading to do. Thanks! The only problem is that I don't seem to be receiving any of the events from the stream so the stream is never being provided. I put a print statement in the await for loop you posted to make sure. However, events fire from: `final auth = useProvider(AuthService.provider); return auth.when(...` The provider is being created but since no auth events ever seem to go through, the stream is never created. Any thoughts? – Alex Hartford Jul 14 '20 at 20:48
  • For some reason, this seems to only listen to the first auth stream event. I can put an external listener and see the events, but the User provider only ever receives the first one. Can't figure out what's going on here... – Alex Hartford Jul 15 '20 at 14:41
  • After signout the change is not received, yes. – SalahAdDin Jul 05 '21 at 14:32
  • Is it correct that watching a StreamProvider is lossy, i.e. all values until the next vsync/rebuild are dropped except the last one? – user3612643 Mar 02 '23 at 20:43
  • @user3612643 It's not a property of StreamProvider but widgets. Their rebuild is debounced in a way – Rémi Rousselet Mar 03 '23 at 08:27
1

I want to preface this by saying I've only been working with Dart/Flutter for a few months, so feel free to correct anything I say and I will update the answer.

I solved this issue after much trial and error and re-reviewing documentation many times. I guess I made a poor assumption that Providers would update when a Provider they depend on changes.

I found that once a StreamProvider returns (or yields), until it is disposed it will always return the same value it originally did, regardless of dependency changes or events coming from a Stream. This is where Computed is useful but doesn't really work well when you desire to return AsyncValue from your provider (how StreamProvider behaves).

Also, additional confusion was caused by the Flutter Inspector not updating the ProviderScope widget correctly. I have to click around and refresh a few times to see updates to Providers or their state. Anyways...

class AuthService {
  ...
  static final provider = StreamProvider.autoDispose((ref) {
    final sub = FirebaseAuth.instance.onAuthStateChanged.listen((auth) => ref.read(uidProvider).state = auth?.uid);
    ref.onDispose(() => sub.cancel());
    return FirebaseAuth.instance.onAuthStateChanged;
  });

  static final uidProvider = StateProvider<String>((_) => null);
  ...
}
class User {
  ...
  static final provider = StreamProvider.autoDispose((ref) {
    final uid = ref.read(AuthService.uidProvider)?.state;
    return Service.user.stream(uid);
  });
  ...

This solution works given that your UI no longer depends on providers (allowing them to be properly disposed) when a user signs out of your app.

Alex Hartford
  • 5,110
  • 2
  • 19
  • 36
  • Re: "once a StreamProvider returns (or yields), until it is disposed it will always return the same value it originally did" I don't think this is correct ... I'd bet if you put a print/log INSIDE the constructor for that Stream Provider, you will see it's getting rebuilt over and over again. Thats why you keep getting the initial value. – Dewey Nov 30 '20 at 04:51