How to convert a Stream to a Listenable in Flutter?
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;
}
}
Florian Leeser
Updated on November 28, 2022Comments
-
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 therefreshListenable
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 over 2 yearscreate a class that extends
ChangeNotifier
and callnotifyListeners
when yourStream.authState
changes - useStream.listen
to listen for any new events from that stream -
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 over 2 yearsbut you have a
Stream
dont you? if so, you can listen that stream -
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 over 2 years@FlorianLeeser Hi) Do you find solution? Can You provide it here, please?
-
Florian Leeser over 2 years@Konstantin Unfortunately I did not. There seems to be no way this can be done.
-
-
Florian Leeser over 2 yearsThis 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 over 2 yearsThe 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 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 about 2 yearsThis is the best way to go!