How to convert a Stream to a Listenable in Flutter?
Asked Answered
B

2

8

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;
    }
)
Bills answered 11/11, 2021 at 15:10 Comment(6)
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 streamCircumlocution
@Circumlocution But since I am using Riverpod's ChangeNotifierProvider I am not able to read this Notifier inside the GoRouter constructor.Bills
but you have a Stream dont you? if so, you can listen that streamCircumlocution
@Circumlocution 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().Bills
@FlorianLeeser Hi) Do you find solution? Can You provide it here, please?Aquilegia
@Konstantin Unfortunately I did not. There seems to be no way this can be done.Bills
P
17

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

GoRouterRefreshStream(_fooBloc.stream)

UPDATE: 20/9/2022

This class is removed from go_router 5.0, and there is no alternative class provided in go_router. The reason being that this class is unrelated to routing and should not be included in go_router. To migrate existing applications that use this class, one can copy the class to their code and own the implementation directly.

import 'dart:async';

import 'package:flutter/foundation.dart';

class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Stream<dynamic> stream) {
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen(
          (dynamic _) => notifyListeners(),
        );
  }

  late final StreamSubscription<dynamic> _subscription;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}
Physiotherapy answered 18/3, 2022 at 20:11 Comment(2)
This is the best way to go!Bills
Is there a full example using firebase authentication anywhere?Rosio
R
3

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;
  }
}
Requite answered 5/1, 2022 at 20:52 Comment(3)
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.Bills
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.Bills
@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.Requite

© 2022 - 2025 — McMap. All rights reserved.