Perché l'autofill non è un dettaglio
La schermata di login è il punto in cui si perde il maggior numero di utenti. Ogni carattere digitato a mano è attrito: una password lunga, un codice OTP da leggere nella notifica, il passaggio avanti e indietro tra app e SMS. Sia Android sia iOS offrono da anni meccanismi di compilazione automatica (password manager di sistema, SMS Retriever, suggerimento del codice sulla tastiera) e Flutter li espone con API semplici, che però restano poco usate perché richiedono qualche accortezza in più rispetto a un normale TextField.
In questa guida vediamo come:
- marcare correttamente i campi con
autofillHintse raggrupparli conAutofillGroup; - far comparire il prompt "Vuoi salvare la password?" dopo un login riuscito;
- configurare Android e iOS perché il sistema riconosca la nostra app;
- leggere automaticamente un codice OTP arrivato via SMS.
Le basi: autofillHints e AutofillGroup
Il primo passo è dichiarare la semantica di ogni campo. Il sistema operativo non indovina: se non gli diciamo che quel campo è uno username, non proporrà nulla.
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.username, AutofillHints.email],
)
La classe AutofillHints contiene decine di costanti (username, password, newPassword, oneTimeCode, telephoneNumber, postalCode, creditCardNumber, name, streetAddressLine1…). Ogni piattaforma mappa questi hint sulle proprie costanti native; non tutte sono supportate ovunque, ma gli hint sconosciuti vengono semplicemente ignorati.
Il secondo passo è il raggruppamento. Un password manager salva una coppia credenziali: perché funzioni, username e password devono vivere nello stesso AutofillGroup.
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _email = TextEditingController();
final _password = TextEditingController();
@override
void dispose() {
_email.dispose();
_password.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AutofillGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _email,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.username],
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.password],
onSubmitted: (_) => _login(),
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 24),
FilledButton(onPressed: _login, child: const Text('Accedi')),
],
),
);
}
}
In fase di registrazione usa
AutofillHints.newPasswordinvece dipassword: è il segnale che fa proporre al portachiavi una password forte generata automaticamente.
Far salvare le credenziali: finishAutofillContext
Dichiarare gli hint serve a leggere i dati. Per scriverli (cioè per ottenere il prompt "Salva password") bisogna dire esplicitamente al sistema che il contesto di autofill è concluso, tipicamente quando il login va a buon fine:
import 'package:flutter/services.dart';
Future<void> _login() async {
final result = await authRepository.signIn(_email.text, _password.text);
if (result.isSuccess) {
// Chiude il contesto e chiede al sistema di salvare i dati inseriti.
TextInput.finishAutofillContext();
if (mounted) context.go('/home');
} else {
// Credenziali errate: non salvare nulla.
TextInput.finishAutofillContext(shouldSave: false);
_showError(result.error);
}
}
Il parametro shouldSave: false è importante: evita che il password manager memorizzi una password sbagliata, errore classico che poi costringe l'utente a correggere la voce salvata.
Se l'utente abbandona la schermata senza completare il login, puoi controllare il comportamento con onDisposeAction:
AutofillGroup(
onDisposeAction: AutofillContextAction.cancel, // default: commit
child: ...,
)
Login a più step
Se la tua UI chiede prima l'email e poi, in una seconda schermata, la password (pattern "identifier first"), i due campi non possono stare nello stesso AutofillGroup. In quel caso l'autofill funziona comunque in lettura, ma il salvataggio della coppia è meno affidabile: valuta un unico step con entrambi i campi, oppure mantieni la password nella stessa route usando un PageView interno — i widget restano nello stesso albero e quindi nello stesso gruppo.
Configurazione per piattaforma
iOS
Su iOS il Password AutoFill funziona già con i soli hint, ma per far proporre le credenziali del sito web corrispondente devi collegare app e dominio:
- aggiungi la capability Associated Domains in Xcode con
webcredentials:tuodominio.it; - pubblica il file
apple-app-site-associationsuhttps://tuodominio.it/.well-known/apple-app-site-associationincludendo la sezionewebcredentialscon il tuoTEAMID.bundle.id.
È la stessa infrastruttura dei Universal Links: se l'hai già configurata per il deep linking, ti serve solo aggiungere la chiave webcredentials.
Android
L'Autofill Framework è disponibile da API 26 e funziona automaticamente con gli hint. Per l'associazione con il sito web (Google Password Manager) serve il file Digital Asset Links su https://tuodominio.it/.well-known/assetlinks.json, con la relazione delegate_permission/common.get_login_creds oltre a quella dei deep link.
Attenzione a un dettaglio pratico: alcuni servizi di autofill di terze parti mostrano il pannello solo se l'app non blocca gli screenshot (FLAG_SECURE). Se usi flag di sicurezza sulla schermata di login, testala con il password manager reale.
Web e desktop
Su Flutter Web gli autofillHints vengono mappati sull'attributo autocomplete degli input HTML, quindi il browser propone le credenziali salvate. Il supporto è però parziale e dipende dal renderer: verificalo sempre sui browser target. Su desktop, al momento, l'autofill di sistema non è supportato.
OTP via SMS: le tre strade
L'altro grande punto di attrito è il codice di verifica. Qui le piattaforme si comportano in modo molto diverso.
iOS: basta un hint
Su iOS non serve alcun SDK: se il campo dichiara AutofillHints.oneTimeCode, il sistema analizza l'SMS in arrivo e propone il codice sopra la tastiera (QuickType).
TextField(
controller: _otp,
keyboardType: TextInputType.number,
autofillHints: const [AutofillHints.oneTimeCode],
)
Perché funzioni bene, il messaggio deve contenere una formula riconoscibile, ad esempio: Il tuo codice di verifica è 123456.
Android: SMS Retriever o User Consent
Android offre due API dei Google Play Services:
- SMS Retriever API: nessun permesso, lettura totalmente automatica, ma il messaggio deve iniziare con
<#>e terminare con un hash di 11 caratteri che identifica la tua app (dipende da package name e certificato di firma). - SMS User Consent API: nessun hash necessario, ma l'utente deve confermare con un dialog di sistema il permesso a leggere quel singolo messaggio.
Un SMS compatibile con SMS Retriever ha questa forma:
<#> Il tuo codice Acme è 123456
FA+9qCX9VSu
L'hash va generato una volta per ogni variante di firma (debug, release, Play App Signing) e passato al backend che invia gli SMS.
Implementazione con Pinput e smart_auth
Il pacchetto pinput fornisce la UI a caselle per l'OTP e integra nativamente l'ascolto degli SMS su Android tramite smart_auth:
Pinput(
length: 6,
controller: _otpController,
keyboardType: TextInputType.number,
// iOS: suggerimento sulla tastiera
// Android: ascolto automatico dell'SMS
androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsRetrieverApi,
onCompleted: (code) => _verify(code),
defaultPinTheme: PinTheme(
width: 56,
height: 56,
textStyle: Theme.of(context).textTheme.titleLarge,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
),
)
Se preferisci gestire tutto a mano, smart_auth espone le stesse funzionalità, compreso il recupero dell'app signature da comunicare al backend:
final smartAuth = SmartAuth();
// Da stampare in debug e configurare lato server.
final signature = await smartAuth.getAppSignature();
debugPrint('App signature: $signature');
// Avvia l'ascolto prima di richiedere l'invio dell'SMS.
final res = await smartAuth.getSmsCode(useUserConsentApi: false);
if (res.succeed && res.code != null) {
_otpController.text = res.code!;
_verify(res.code!);
}
// Quando esci dalla schermata
await smartAuth.removeSmsListener();
L'API di smart_auth è cambiata tra la versione 1.x e la 2.x: controlla il changelog del pacchetto prima di copiare il codice. L'ordine delle operazioni, invece, resta sempre lo stesso: avvia l'ascolto, poi chiedi al backend di inviare l'SMS. Se inverti i due passaggi, un SMS veloce può arrivare prima che il listener sia attivo.
Errori frequenti
- Hint su un solo campo. Senza il campo password nello stesso gruppo, molti password manager non propongono il salvataggio.
- Dimenticare
finishAutofillContext(). L'autofill in lettura funziona, ma nessuna credenziale viene mai memorizzata: l'utente dovrà digitare per sempre. - Salvare password errate. Chiama sempre
finishAutofillContext(shouldSave: false)nel ramo di errore. - Hash SMS sbagliato. L'hash della build di debug è diverso da quello della release firmata con Play App Signing: servono entrambi lato backend, altrimenti l'autofill funziona in locale e non in produzione.
- Campo OTP ricostruito. Se il widget dell'OTP viene smontato e rimontato (ad esempio da un
setStateche cambia la struttura dell'albero), il listener SMS può perdersi: gestisci lo stato di caricamento senza sostituire il widget. - Test solo su emulatore. L'autofill dipende dal password manager installato: provalo su dispositivi reali, con Google Password Manager, iCloud Keychain e almeno un manager di terze parti.
Oltre le password: le passkey
Il passo successivo logico è eliminare del tutto la password. Le passkey (WebAuthn/FIDO2) sono supportate su Android tramite Credential Manager e su iOS tramite il portachiavi, e richiedono la stessa configurazione di associated domains / asset links vista sopra. In Flutter si usano con pacchetti come credential_manager o i plugin dei provider di identità (Firebase Auth, Supabase, Auth0). Se hai già collegato app e dominio per l'autofill, hai fatto gran parte del lavoro necessario anche per le passkey.
Conclusione
L'autofill è una di quelle funzionalità a basso costo e alto impatto: poche righe di codice, un po' di configurazione lato piattaforma e un login che passa da trenta secondi a due tap. Comincia dagli autofillHints e da AutofillGroup, aggiungi finishAutofillContext() nel flusso di successo, poi occupati dell'OTP. E verifica sempre su dispositivi reali: è l'unico modo per sapere se il password manager dell'utente sta davvero collaborando.