Patrick Obafemi
Patrick Obafemi

Reputation: 1066

Future Provider Stuck In loading state

I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider

final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));

In my UI I have this....

class LoginScreen extends HookWidget {
   final TextEditingController emailEditingController = TextEditingController();
   final TextEditingController passwordEditingController =
       TextEditingController();

  @override
  Widget build(BuildContext context) {
     var userInput =
     UserInput(emailEditingController.text, passwordEditingController.text);

     final login = useProvider(loginProvider(userInput));

     return login.when(
       data: (user) => Login(emailEditingController, passwordEditingController),
       loading: () => const ProgressIndication(),
       error: (error, stack) {
         if (error is DioError) {
         return Login(emailEditingController, passwordEditingController);
       } else {
          return Login(emailEditingController, passwordEditingController);
       }
     },
  );
 }
}

here is my doLogin function.

@override
  Future<dynamic> doLogin(String email, String password) async {
    try {
       final response = await _read(dioProvider)
          .post('$baseUrl/login', data: {'email': email, 'password': password});
       final data = Map<String, dynamic>.from(response.data);
     return data;
    } on DioError catch (e) {
       return BadRequestException(e.error);
    } on SocketException {
      return 'No Internet Connection';
   }
 }

I would like to know why it's stuck in the loading state. Any help will be appreciated.

Upvotes: 1

Views: 1816

Answers (1)

Alex Hartford
Alex Hartford

Reputation: 6010

First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.

In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.

The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.

First, a StateNotifierProvider:

enum LoginState { loggedOut, loading, loggedIn, error }

class LoginStateNotifier extends StateNotifier<LoginState> {
  LoginStateNotifier(this._read) : super(LoginState.loggedOut);

  final Reader _read;
  late final Map<String, dynamic> _user;

  static final provider =
      StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));

  Future<void> login(String email, String password) async {
    state = LoginState.loading;
    try {
      _user = await _read(authRepositoryProvider).doLogin(email, password);
      state = LoginState.loggedIn;
    } catch (e) {
      state = LoginState.error;
    }
  }

  Map<String, dynamic> get user => _user;
}

This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.

Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.

class LoginScreen extends HookWidget {
  const LoginScreen({Key? key, this.error = false}) : super(key: key);

  final bool error;

  @override
  Widget build(BuildContext context) {
    final emailController = useTextEditingController();
    final passwordController = useTextEditingController();

    return Column(
      children: [
        TextField(
          controller: emailController,
        ),
        TextField(
          controller: passwordController,
        ),
        ElevatedButton(
          onPressed: () async {
            await context.read(LoginStateNotifier.provider.notifier).login(
                  emailController.text,
                  passwordController.text,
                );
          },
          child: Text('Login'),
        ),
        if (error) Text('Error signing in'),
      ],
    );
  }
}

You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.

Last but not least, we need to do something with our fancy new state.

class AuthPage extends HookWidget {
  const AuthPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final loginState = useProvider(LoginStateNotifier.provider);

    switch (loginState) {
      case LoginState.loggedOut:
        return LoginScreen();
      case LoginState.loading:
        return LoadingPage();
      case LoginState.loggedIn:
        return HomePage();
      case LoginState.error:
        return LoginScreen(error: true);
    }
  }
}

In practice, you're going to want to wrap this in another widget with a Scaffold.

I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.

Upvotes: 3

Related Questions