skip to content

In Flutter, how do you build a password TextFormField with a show/hide toggle that password managers can fill and save?

level: juniorimportance: should knowfreq 44%

answer

  1. obscureText, default false
  2. masked fields stay single-line
  3. a State bool behind suffixIcon
  4. AutofillHints.password vs newPassword
  5. AutofillGroup commits on dispose

basics

~20 s

Drive obscureText from a bool in State and flip it with setState from an IconButton in InputDecoration.suffixIcon. Add autofillHints such as AutofillHints.username and AutofillHints.password, and wrap the credential fields in an AutofillGroup so the platform can offer and save them.

solid answer

~30 s

`obscureText` defaults to `false`; setting it masks the display with `obscuringCharacter` (default `•`) but the controller still holds plain text. An obscured field must be single-line; the constructor asserts `Obscured fields cannot be multiline.` For the toggle I keep `bool _hidden = true` in `State` and put an `IconButton` in `InputDecoration.suffixIcon` that calls `setState`. For password managers I set `autofillHints: const [AutofillHints.username]` and `[AutofillHints.password]` (or `AutofillHints.newPassword` on sign-up) and wrap both fields in an `AutofillGroup`. Its default `onDisposeAction`, `AutofillContextAction.commit`, asks the platform to save the credentials when the group is disposed; `TextInput.finishAutofillContext()` does it explicitly.

code

dart · 39 lines
dart
import 'package:flutter/material.dart';

class OwnerSignInFields extends StatefulWidget {
  const OwnerSignInFields({super.key});

  @override
  State<OwnerSignInFields> createState() => _OwnerSignInFieldsState();
}

class _OwnerSignInFieldsState extends State<OwnerSignInFields> {
  bool _hidden = true;

  @override
  Widget build(BuildContext context) {
    return AutofillGroup(
      child: Column(
        children: [
          TextFormField(
            keyboardType: TextInputType.emailAddress,
            autofillHints: const [AutofillHints.username],
            decoration: const InputDecoration(labelText: 'Email'),
          ),
          TextFormField(
            obscureText: _hidden,
            autofillHints: const [AutofillHints.password],
            decoration: InputDecoration(
              labelText: 'Password',
              suffixIcon: IconButton(
                tooltip: _hidden ? 'Show password' : 'Hide password',
                icon: Icon(_hidden ? Icons.visibility : Icons.visibility_off),
                onPressed: () => setState(() => _hidden = !_hidden),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

go deeper

for a junior

Know obscureText, its single-line rule, and the setState toggle in suffixIcon, and that the controller still holds the plain password.

for a middle

Explain autofill: hints per field, the empty-list default versus null, and how AutofillGroup commits or cancels the context when it is disposed.

for a senior

Get credential saving right across platforms: commit only after a successful sign-in, use newPassword on sign-up, and account for Android API levels and iOS associated domains.

for a principal

Decide what a shared credential-field widget must enforce by default so every team's sign-in and sign-up screens behave consistently with password managers.

## obscureText basics `TextFormField` (and `TextField`) take **`obscureText`**, which defaults to `false`. When `true`: - each character is drawn as **`obscuringCharacter`**, default `'•'` (the constructor asserts it is exactly one character); - on platforms whose system setting allows it (`brieflyShowPassword`), the character just typed is shown briefly before being masked; - the field must be **single-line**: `maxLines` must be 1, and the constructor asserts `Obscured fields cannot be multiline.` What `obscureText` does **not** do is protect the value. The `TextEditingController`, the validator, `onChanged` and `onSaved` all see plain text. Anything that must stay secret after entry, such as never logging it or clearing it from memory once used, is your code's responsibility. ## The show/hide toggle The toggle is ordinary widget state: a `bool` in the `State`, read by `obscureText`, flipped by an `IconButton` placed in the decoration's `suffixIcon`. The same controller, or the field's internal one, keeps the text while the flag flips, so no text is lost. Give the button a `tooltip`, because the icon alone does not tell every user what it does. ## Autofill hints **`autofillHints`** tells the platform's autofill service what a field is for. It takes strings, normally constants from **`AutofillHints`**: | Field | Hint | |---|---| | Sign-in user name or e-mail | `AutofillHints.username` or `AutofillHints.email` | | Sign-in password | `AutofillHints.password` | | Sign-up or change-password field | `AutofillHints.newPassword` | | SMS or e-mail verification code | `AutofillHints.oneTimeCode` | The default is an **empty list**. Passing `null` instead opts the field out: it stops sending autofill information to the platform, and on Android and the web it disables autofill for that field. Material's `TextField` turns a null `keyboardType` into `TextInputType.text` for a single-line field, so set a matching type yourself, such as `TextInputType.emailAddress` for an e-mail field. ## AutofillGroup and saving credentials Password managers work on a **context**, the set of fields filled together, rather than on single fields: 1. Wrap the user-name and password fields in an **`AutofillGroup`**, often around the whole `Form`. 2. The platform can then fill both fields at once when the user picks a saved credential. 3. When the topmost `AutofillGroup` is disposed, for instance because a successful sign-in replaced the route, its **`onDisposeAction`** runs. The default, **`AutofillContextAction.commit`**, tells the platform to save the entered values and end the context; **`AutofillContextAction.cancel`** ends it without saving. 4. To decide yourself, call **`TextInput.finishAutofillContext()`** (`shouldSave` defaults to `true`) after a sign-in succeeds, or `finishAutofillContext(shouldSave: false)` after it fails. ## Validating password fields The masking changes nothing about validation; a password field takes a `validator` like any other `TextFormField`: - On sign-in, check only that the field is not empty. Rules about length or character classes belong to sign-up; applying them at sign-in rejects passwords the server would accept. - On sign-up, a field-level `autovalidateMode` of `onUserInteraction` lets the user watch a rule such as a minimum length being met as they type. - A "confirm password" field needs the first field's value: give the first field a `TextEditingController` owned by the `State`, and compare against `_password.text` in the second field's validator. Because the form rebuilds every field when any value changes, the comparison stays current. - Never log the value, and avoid copying it into long-lived state in `onSaved`; hand it straight to the sign-in call. ## Platform notes - Android autofill needs API level 26 or later. - iOS password autofill also depends on the app's associated domains, which are configured on the iOS side rather than in Dart. - Some iOS hints only work with a compatible `keyboardType`; the framework documentation names `AutofillHints.email` with `TextInputType.emailAddress`. ## Mistakes to avoid - Believing `obscureText` encrypts or hides the value from your own code. - Marking a sign-up password with `AutofillHints.password`, which suggests an existing credential instead of a new one. - Recreating the controller when the visibility flips, which loses the text. - Setting `autofillHints: null` while expecting autofill to work.

  • Why does a TextFormField with obscureText: true and maxLines: 3 fail?
    The constructor asserts `!obscureText || maxLines == 1` with the message `Obscured fields cannot be multiline.` Masked input is single-line only, so leave `maxLines` at its default of 1 for password fields.
  • When would you finish the autofill context without saving?
    When the credentials should not be stored, for example after the server rejected the sign-in. Call `TextInput.finishAutofillContext(shouldSave: false)`, or build the `AutofillGroup` with `onDisposeAction: AutofillContextAction.cancel`, so the platform does not offer to save a wrong password.
  • Which hint belongs on a sign-up form's password field, and why?
    `AutofillHints.newPassword`. It tells the autofill service the field expects a new credential rather than a stored one, so the service can offer to generate or save a password instead of filling an existing one. `AutofillHints.password` is for signing in.

saying these in an interview costs you the question

  • obscureText encrypts the value held in the controller.
  • An obscured field can be made multi-line with maxLines.
  • Setting autofillHints to null is the same as leaving the default.
  • Toggling visibility needs a new TextEditingController each time.
  • AutofillHints.password is the right hint for a sign-up password field.