How to convert a Stream to a Listenable in Flutter?

565

Solution 1

According to the go_router documentation, you can simply use the following method: https://gorouter.dev/redirection#refreshing-with-a-stream

GoRouterRefreshStream(_fooBloc.stream)

Solution 2

I don't really know how I would do this using riverpod, but I think you don't need context for that using riverpod. With Provider I would do something like this:

  // Somewhere in main.dart I register my dependencies with Provider:

      Provider(
        create: (context) =>  AuthService(//pass whatever),
     // ...

  // Somewhere in my *stateful* App Widget' State:
  // ...
  late ValueListenable<bool> isLoggedInListenable;

  @override
  void initState(){
    // locate my authService Instance
    final authService = context.read<AuthService>();
    // as with anything that uses a stream, you need some kind of initial value
    // "convert" the stream to a value listenable
    isLoggedInListenable = authService.isLoggedIn.toValueListenable(false);
    super.initState();
  }

  @override
  Widget build(BuildContext context){
    final router = GoRouter(
      refreshListenable: isLoggedInListenable,
      redirect: (GoRouterState state) {
        bool isLoggedIn = isLoggedInListenable.value;
      
        if (!isLoggedIn && !onAuthRoute) //redirect to /signin;
        // ...
      }
    );
    return MaterialApp.router(
      // ...
    );
  }

And this is the extension to "convert" streams to ValueListenable

extension StreamExtensions<T> on Stream<T> {
  ValueListenable<T> toValueNotifier(
    T initialValue, {
    bool Function(T previous, T current)? notifyWhen,
  }) {
    final notifier = ValueNotifier<T>(initialValue);
    listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }

  // Edit: added nullable version
  ValueListenable<T?> toNullableValueNotifier{
    bool Function(T? previous, T? current)? notifyWhen,
  }) {
    final notifier = ValueNotifier<T?>(null);
    listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }

  Listenable toListenable() {
    final notifier = ChangeNotifier();
    listen((_) {
      // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
      notifier.notifyListeners();
    });
    return notifier;
  }
}

It works because ValueListenable is a Listenable! same as ChangeNotifier (it just also holds data).

In your case, if you can get ahold your instance of authService before declaring the router, you can convert the stream to a listenable and then use it. Make sure it's part of the widget's state, otherwise you might get the notifier garbage collected. Also, I added a notifyWhen method in case you want to filter by a condition the notifications. In this case is not needed and the ValueNotifier will only notify if the value actually changed.

And to add a bit more, for people using flutter_bloc this extension works too:

extension BlocExtensions<T> on BlocBase<T> {
  Listenable asListenable() {
    final notifier = ChangeNotifier();
    stream.listen((_) {
      // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
      notifier.notifyListeners();
    });
    return notifier;
  }

  ValueListenable<T> asValueListenable({
    BlocBuilderCondition? notifyWhen,
  }) {
    final notifier = ValueNotifier<T>(state);
    stream.listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }
}
Share:
565
Florian Leeser
Author by

Florian Leeser

Updated on November 28, 2022

Comments

  • Florian Leeser
    Florian Leeser over 1 year

    I am trying to figure out how to make use of Firebase's onAuthStateChanges() stream to use as a Listenable in the refreshListenable parameter from the go_router package to redirect whenever the authState changes. In additon I am using flutter_riverpod for State Mangement.

    My code looks like this so far:

    I created a simple AuthService class (shrinked down to the most important parts):

    abstract class BaseAuthService {
      Stream<bool> get isLoggedIn;
      Future<bool> signInWithEmailAndPassword({ required String email, required String password });
    }
    
    class AuthService implements BaseAuthService {
      final Reader _read;
    
      const AuthService(this._read);
    
      FirebaseAuth get auth => _read(firebaseAuthProvider);
    
      @override
      Stream<bool> get isLoggedIn => auth.authStateChanges().map((User? user) => user != null);
    
      @override
      Future<bool> signInWithEmailAndPassword({ required String email, required String password }) async {
        try {
          await auth.signInWithEmailAndPassword(email: email, password: password);
          return true;
        } on FirebaseAuthException catch (e) {
          ...
        } catch (e) {
          ...
        }
    
        return false;
      }
    

    Next I created these providers:

    final firebaseAuthProvider = Provider.autoDispose<FirebaseAuth>((ref) => FirebaseAuth.instance);
    
    final authServiceProvider = Provider.autoDispose<AuthService>((ref) => AuthService(ref.read));
    

    As mentioned before, I would like to somehow listen to these authChanges and pass them to the router:

    final router = GoRouter(
        refreshListenable: ???
        redirect: (GoRouterState state) {
            bool isLoggedIn = ???
            
            if (!isLoggedIn && !onAuthRoute) redirect to /signin;
        }
    )
    
    • pskink
      pskink over 2 years
      create a class that extends ChangeNotifier and call notifyListeners when your Stream.authState changes - use Stream.listen to listen for any new events from that stream
    • Florian Leeser
      Florian Leeser over 2 years
      @pskink But since I am using Riverpod's ChangeNotifierProvider I am not able to read this Notifier inside the GoRouter constructor.
    • pskink
      pskink over 2 years
      but you have a Stream dont you? if so, you can listen that stream
    • Florian Leeser
      Florian Leeser over 2 years
      @pskink Yes, I am able to listen to that Stream and also notify the listeners when the authState changes, but as I said, somehow I have to provide the ChangeNotifier to the GoRouter constructor, where there is no context or ref I can refer to and call something like context.read().
    • Constantine
      Constantine over 2 years
      @FlorianLeeser Hi) Do you find solution? Can You provide it here, please?
    • Florian Leeser
      Florian Leeser over 2 years
      @Konstantin Unfortunately I did not. There seems to be no way this can be done.
  • Florian Leeser
    Florian Leeser over 2 years
    This works like a charm!!! I am so thankful! One more question: Isn't it somehow possible to pass null as the initialValue to the converter? Because at the beginning the Auth-Stream yields nothing until it knows wether the user is logged in or not, right? Because if so, I wanted to show kind of a Splash Screen at the start until the loading is over. Otherwise it would show the LoginScreen for a few milliseconds.
  • Florian Leeser
    Florian Leeser over 2 years
    The problem here clearly appears whenever I enter a path on the Web. When calling /home for example, everything is reloading and showing the SignInScreen for a brief moment, because the Listenable is yielding false at the beginning.
  • nosmirck
    nosmirck over 2 years
    @Florian Lesser just edited it and added a nullable version for it, so, you can safely call it without any initial value and it should be null initially.
  • Florian Leeser
    Florian Leeser about 2 years
    This is the best way to go!