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?
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:formValidator
changes on submit (new state is with new form validator value emitted), so it causes entire form (includingAutofillGroup
) 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:
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.