skip to Main Content

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


  1. I know it sounds overwhelming but why not try to use a completer and wait for it:

    final meProvider = FutureProvider<Profile>((ref) async {
      final completer = Completer<Profile>();
    
      ref.watch(authProvider);
      var auth = ref.watch(authProvider.notifier);
    
      if (auth.isLoggedIn()) {
        Profile profile = await auth.api().getOwnProfile();
        logger.d("Own Profile: $profile");
        completer.complete(profile);
      } else {
        logger.d("OwnProfile: null");
      }
      
      return completer.future;
    });
    

    As for your questions:

    How can I make this nice

    Code above prevents null values so its nice for now

    Can I only trigger the watch of the FutureProvider under the condition
    of a certain state in the authProvider?

    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)...)

    Can I maybe ‘force’ the FutureProvider to stick in the loading state

    After the first build you can use ref.state inside your listeners, 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:

    var api = ref.watch(APIProvider.notifier);
    
    Login or Signup to reply.
  2. @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 of authNotifier by saying that meProvider always returns a Profile, 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) with ref.watch(authProvider.select(state) => state == const AuthStateLoggedIn()); should do the trick – you shouldn’t even need to check the return value, as the FutureProvider will not get the notification to update itself.

    For a cleaner solution, I’d see two options:

    • Couple the Profile to AuthStateLoggedIn and add the method to fetch the profile to the authNotifier. This will work, but the response will take longer (2 sequential requests) and you’d have to listen to the state of authNotifier instead (by which you’ll lose all AsyncValue features, such as .when).
    • Depending on your Riverpod version, take a look at AsyncNotifierProvider. You can replace your FutureProvider with that, which will give you full control over your value/loading/error states and also returns an AsyncValue. (Bonus: ref is always passed by default and you can use the new @riverpod code generation features 😉)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search