skip to Main Content

My code to show and submit login form:

class LoginWidget extends StatelessWidget {
  const LoginWidget({
    super.key,
  });

 @override
  Widget build(BuildContext context) {
    final citiesLoaded = context.select((LoginBloc bloc) => bloc.state.citiesLoaded);
    final loginBloc = context.read<LoginBloc>();

    final selectedCity = (citiesLoaded && loginBloc.state.cities.length == 1)
        ? loginBloc.state.cities[0]
        : context.select((LoginBloc bloc) => bloc.state.selectedCity);

    final formValidator = context.select((LoginBloc bloc) => bloc.state.formValidator);

    final theme = Theme.of(context).extension<LoginPageThemeData>();

    return AutofillGroup(
      child: Column(
        children: [
          citiesLoaded
              ? Visibility(
                  visible: loginBloc.state.cities.length > 1,
                  child: CitySelectWidget(selectedCity: selectedCity),
                )
              : const BusyWidget(status: 'Loading cities'),
          Text('Username:'),
          TextFormField(
              autofillHints: const [AutofillHints.username, AutofillHints.email, AutofillHints.name],
              enabled: selectedCity != null,
              initialValue: loginBloc.state.username,
              keyboardType: TextInputType.emailAddress,
              decoration: theme?.inputDecoration
                  ?.copyWith(errorText: formValidator.usernameField.isValid ? null : formValidator.usernameField.validationMessage),
              onChanged: (value) => loginBloc.add(SetUsernameEvent(username: value))),
          Text('Password:'),
          TextFormField(
              autofillHints: const [AutofillHints.username, AutofillHints.email, AutofillHints.name],
              enabled: selectedCity != null,
              initialValue: loginBloc.state.password,
              obscureText: true,
              decoration: theme?.inputDecoration
                  ?.copyWith(errorText: formValidator.passwordField.isValid ? null : formValidator.passwordField.validationMessage),
              onChanged: (value) => loginBloc.add(SetPasswordEvent(password: value))),
          ElevatedButton(
            onPressed: selectedCity == null
                ? null
                : () {
                    context.read<app_bloc.AppBloc>().add(app_bloc.SetSelectedCityEvent(selectedCity));
                    context.read<LoginBloc>().add(const AuthEvent());
                  },
            child: Text('Sign in'),
          ),
        ],
      ),
    );
  }
}

In parent widget (login page) to show main app’s page when aut process finished:

 @override
  Widget build(BuildContext context) {
    return BlocListener<LoginBloc, LoginState>(
      listenWhen: (previous, current) => previous.isLogged != current.isLogged || (current.loginFailed && !current.isBusy),
      listener: (context, state) {
        if (state.isLogged) {
          Navigator.pushNamedAndRemoveUntil(context, '/home', (route) => false);
        }
        if (state.loginFailed) {
          Fluttertoast.showToast(msg: state.statusMessage);
        }
      },
      child: Scaffold( ......

So,on submit isLogged flag will be set on after auth success and app navigates to main main page + destroy login page. But autofill still does not work. No prompt to save password.

Any ideas what else I need to do?

2

Answers


  1. Chosen as BEST ANSWER

    I tested official flutter bloc example - login flow. Except they use formz library to simplify form validation, it's almost the same way as mine. So I added AutofillGroup there and it worked. I finally found reason why it does not work in my case. It's because I used:

    final formValidator = context.select((LoginBloc bloc) => bloc.state.formValidator);
    

    formValidator changes on submit (new state is with new form validator value emitted), so it causes entire form (including AutofillGroup) rebuild. It seems in this case, autofill will lost its context and there's no any data to save anymore.

    Solution is to drop top level select and use this instead, to make sure only form items will be refreshed if required:

     BlocBuilder<LoginBloc, LoginState>(
                buildWhen: (previous, current) => previous.password != current.password || previous.formValidator != current.formValidator,
                builder: (context, state) {
                  return TextFormField(
                      autofillHints: const [AutofillHints.password],
                      enabled: selectedCity != null,
                      initialValue: loginBloc.state.password,
                      obscureText: true,
                      decoration: theme.inputDecoration
                          ?.copyWith(errorText: state.formValidator.passwordField.isValid ? null : state.formValidator.passwordField.validationMessage, label: Text('Hasło')),
                      onChanged: (value) => loginBloc.add(SetPasswordEvent(password: value)));
                }
    

    Beyond that, still Flutter approach to autofill is not very good. It works fine if autofill service is Google, but it almost never works as should with 3rd party autofill services. On iOS, in general it does not work. Password can be saved to keystore, but autofill dialog does not appear, I need to look for credentials by hand.


  2. There seems to be a few issues with the Autofill functionality, sometimes due to device type, operating system and system settings.

    Autofill service providers rely heavily on autofillHints. Make sure the entries in autofillHints are supported by the autofill service currently in use (the name of the service can typically be found in your mobile device’s system settings).

    Troubleshoot Autofill while in development to come up with a best fit solution for many users.

    But generally, to get the best of experience with it:

    1. It is imperative to define the keyboardType to the exact expected text input.
    2. It is better to just specify one AutofillHint type (all types can be found here) which most readily aligns with you keyboardType or TextInputType, but if you must list more than one group of AutofillHint types, you should start with the one that most closely relates to the keyboardType specified; especially to cater for iOS setup as seen here

    Some autofill hints only work with specific keyboardTypes. For example, AutofillHints.name requires TextInputType.name and AutofillHints.email works only with TextInputType.emailAddress. Make sure the input field has a compatible keyboardType. Empirically, TextInputType.name works well with many autofill hints that are predefined on iOS.

    So you can have

    /
    
    Text('Username:'),
    TextFormField(
        autofillHints: const [AutofillHints.email, AutofillHints.name],
        enabled: selectedCity != null,
        initialValue: loginBloc.state.username,
        keyboardType: TextInputType.emailAddress,
        decoration: theme?.inputDecoration
            ?.copyWith(errorText: formValidator.usernameField.isValid ? null : formValidator.usernameField.validationMessage),
        onChanged: (value) => loginBloc.add(SetUsernameEvent(username: value))),
    Text('Password:'),
    TextFormField(
        keyboardType: TextInputType.visiblePassword,
        autofillHints: const [AutofillHints.password],
        enabled: selectedCity != null,
        initialValue: loginBloc.state.password,
        obscureText: true,
        decoration: theme?.inputDecoration
                      ?.copyWith(errorText: formValidator.passwordField.isValid ? null : formValidator.passwordField.validationMessage),
        onChanged: (value) => loginBloc.add(SetPasswordEvent(password: value))),
          
    /
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search