So there is a tricky problem regarding scoped FutureProvider I stumbled upon in Flutter with Riverpod.
I have an authNotifier which is a StateNotifier which is basically my API Service to a login protected service. AuthState
can be loggedOut, loading, loggedIn, error
The constructor sets the initial value to loggedOut
(makes sense).
And during the login function there is first a loading and then loggedIn state change.
Looks like this:
class authNotifier extends StateNotifier<AuthState> {
final Ref ref;
late API _api;
authNotifier(this.ref) : super(const AuthStateLoggedOut()) {
_api = ref.read(APIProvider.notifier);
}
Future<bool> login(String email, String password) async {
state = const AuthStateLoading();
try {
await _api.login(email, password);
} on AuthWrongCredentialsException catch (error) {
state = AuthStateError(error.msg);
return false;
}
state = const AuthStateLoggedIn();
return true;
}
API api() {
return _api;
}
bool isLoggedIn() {
return state == const AuthStateLoggedIn();
}
}
final authProvider = StateNotifierProvider<authNotifier, AuthState>((ref) {
return authNotifier(ref);
});
ok great. that works fine.
BUT!
Now I have a FutureProvider to access the Profile:
final meProvider = FutureProvider<Profile?>((ref) async {
ref.watch(authProvider);
var auth = ref.watch(authProvider.notifier);
if (auth.isLoggedIn()) {
Profile profile = await auth.api().getOwnProfile();
logger.d("Own Profile: $profile");
return profile;
}
logger.d("OwnProfile: null");
return null;
});
and here it starts to get weird from a design perspective.
Because the FutureProvider is called already through watch once the state changes in the authProvider… aka when loading is set and then once the login is successful.
The problem is ONLY during the second it allows me to call the api->getOwnProfile (cause I need a working login to be completed!). So I am forced to return something: hence null
.
This then leads to some minor ugly widget code because now I have to deal with a null special value:
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue meAsync = ref.watch(meProvider);
return meAsync.when(
loading: () => const Spinner(),
data: (data) {
if (data == null) return const Spinner(); //TODO: this is ugly!
return buildProfileWidget(data);
},
error: (Object error, StackTrace stackTrace) => Text(error.toString()),
);
}
Question: How can I make this nice (duh!).
Question: Can I only trigger the watch of the FutureProvider under the condition of a certain state in the authProvider? (scoped / dependencies).
Question: Can I maybe ‘force’ the FutureProvider to stick in the loading state?
2
Answers
I know it sounds overwhelming but why not try to use a completer and wait for it:
As for your questions:
Code above prevents null values so its nice for now
You can use
ref.listen
to listen to changes and use the callback to implement your filtering options or:final isLoggedIn = ref.watch(authProvider.select(state) => state == const AuthStateLoggedIn());
to read when the user is logged in or not (and the if condition will be only the result of that watch value
if(isLoggedIn)...
)After the first build you can use
ref.state
inside yourlisteners
,ref.listeners
to actually set your state as much as you want. Remember that you need to recreate your completer because once is setted you cannot complete it again.Also as a side note, maybe you want to use your api directly instead of the one inside your authProvider:
@EdwynZN’s answer basically says it all in terms of the technical stuff.
In my opinion however, a
Completer
here is not the remedy to an underlying architectural problem – you "assume" the state ofauthNotifier
by saying thatmeProvider
always returns aProfile
, which is fine if you make sure of it in your widget tree (e. g. this widget only gets rendered if the user is logged in). What if you use that widget somewhere else and forget the auth guard? It will always show a spinner, regardless of the auth state.If you don’t want to refactor anything, replacing
ref.watch(authProvider)
withref.watch(authProvider.select(state) => state == const AuthStateLoggedIn());
should do the trick – you shouldn’t even need to check the return value, as theFutureProvider
will not get the notification to update itself.For a cleaner solution, I’d see two options:
Profile
toAuthStateLoggedIn
and add the method to fetch the profile to theauthNotifier
. This will work, but the response will take longer (2 sequential requests) and you’d have to listen to the state ofauthNotifier
instead (by which you’ll lose allAsyncValue
features, such as.when
).AsyncNotifierProvider
. You can replace yourFutureProvider
with that, which will give you full control over your value/loading/error states and also returns anAsyncValue
. (Bonus:ref
is always passed by default and you can use the new@riverpod
code generation features 😉)