skip to content

For a Flutter TextFormField that takes vehicle plate numbers, what do keyboardType, inputFormatters and the validator each guarantee?

level: middleimportance: should knowfreq 38%

answer

  1. a keyboard type is only a request
  2. textCapitalization is a hint too
  3. formatters rewrite each user edit, in order
  4. skipped for programmatic changes
  5. only the validator judges the whole value

basics

~20 s

keyboardType only asks the soft keyboard for a layout; inputFormatters rewrite every user edit, in list order, before the controller and onChanged see it; the validator judges the complete value when validation runs. Only the validator can reject an incomplete or malformed plate.

solid answer

~30 s

`keyboardType` (and `textCapitalization`) are requests to the on-screen keyboard: a hardware keyboard, a paste or another input method can still deliver any character. `inputFormatters` are the enforcement layer for *characters*: each `TextInputFormatter.formatEditUpdate` gets the old and new `TextEditingValue` on every user edit, in list order, and its output reaches the controller before `onChanged`. For a plate I use `FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]'))`, then an uppercasing `TextInputFormatter.withFunction`, plus `maxLength`, which appends a `LengthLimitingTextInputFormatter`. Formatters cannot tell a half-typed plate from a finished one and never run on text set from code, so the `validator` still checks the full pattern at submit.

code

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

final _plateChars = FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]'));
final _upperCase = TextInputFormatter.withFunction(
  (oldValue, newValue) => newValue.copyWith(text: newValue.text.toUpperCase()),
);
final _plateFormat = RegExp(r'^[A-Z0-9]{2,8}$');

TextFormField buildPlateField(TextEditingController controller) {
  return TextFormField(
    controller: controller,
    keyboardType: TextInputType.text,
    textCapitalization: TextCapitalization.characters,
    autocorrect: false,
    enableSuggestions: false,
    maxLength: 8,
    inputFormatters: [_plateChars, _upperCase],
    decoration: const InputDecoration(labelText: 'Plate number'),
    validator: (value) => _plateFormat.hasMatch(value ?? '')
        ? null
        : 'Use 2-8 capital letters or digits',
  );
}

go deeper

for a junior

Know that keyboardType only picks the on-screen keyboard, that inputFormatters filter what the user types, and that the validator still checks the value on submit.

for a middle

Explain the chain: single-line filter, your formatters in order, the maxLength limiter, then the controller and onChanged, and why programmatic text skips it.

for a senior

Design a field that survives paste, hardware keyboards and restored text, keeps its cursor sane under formatting, and leaves the final verdict to validation and the server.

for a principal

Weigh strict per-keystroke formatting against permissive input plus clear validation, and agree which rules belong to shared field widgets.

## Three layers, three jobs A plate-number field in a vehicle-registration app usually stacks three mechanisms. They are easy to confuse because all three seem to "restrict input": | Layer | Runs | Guarantees | |---|---|---| | `keyboardType`, `textCapitalization` | When the field opens the soft keyboard | Nothing; they request a keyboard layout | | `inputFormatters` | On every **user** edit, before the controller updates | Each edit's result, character by character | | `validator` | When validation runs (submit or an autovalidate mode) | The complete value, judged as a whole | ## keyboardType and textCapitalization are hints `keyboardType` takes a `TextInputType` such as `TextInputType.text`, `TextInputType.number`, `TextInputType.phone`, `TextInputType.emailAddress` or `TextInputType.visiblePassword`. If you leave it null, Material's `TextField` uses `TextInputType.text` for a single-line field and `TextInputType.multiline` otherwise. `textCapitalization: TextCapitalization.characters` asks for an upper-case keyboard. Both are sent to the platform's input method, which may honour them. None of them stops: - a hardware or Bluetooth keyboard, or a desktop or web build; - a paste from the clipboard; - a third-party keyboard that ignores the request. So a number keyboard never guarantees digits, and a capitals keyboard never guarantees capitals. ## How inputFormatters run A `TextInputFormatter` implements `formatEditUpdate(oldValue, newValue)` and returns the `TextEditingValue` the field should actually hold. For a single-line `TextField` the chain is: 1. `EditableText` prepends `FilteringTextInputFormatter.singleLineFormatter`, which strips newlines, because `maxLines` is 1. 2. Your formatters run **in list order**; each receives the previous one's output as its new value. 3. If `maxLength` is set, `TextField` appends a `LengthLimitingTextInputFormatter`, so the limit counts the text your formatters produced. 4. The result becomes the controller's value, listeners are notified, and `onChanged` runs last. Two rules follow from the framework's own contract. Formatters **do not run when code changes the text** through the controller, and a changed `inputFormatters` list applies only from the user's **next** edit. Flutter ships `FilteringTextInputFormatter.allow`, `FilteringTextInputFormatter.deny`, the ready-made `FilteringTextInputFormatter.digitsOnly` and `singleLineFormatter`, and `LengthLimitingTextInputFormatter`. There is no built-in upper-case formatter; `TextInputFormatter.withFunction` builds one in a line, as in the code example. ## Filtering versus rejecting `FilteringTextInputFormatter` does not refuse an edit; it **removes** the parts of the new text that do not match (or that do match, for `deny`), optionally substituting `replacementString`, and adjusts the selection to match. Pasting `ab-12 cd` into a plate field filtered to letters and digits therefore yields `ab12cd`, which the uppercase step turns into `AB12CD`. To **reject** an edit outright, write a formatter that returns `oldValue` when the new text breaks a rule: ```dart class MaxDigitsFormatter extends TextInputFormatter { const MaxDigitsFormatter(this.max); final int max; @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { final digits = newValue.text.replaceAll(RegExp(r'[^0-9]'), '').length; return digits > max ? oldValue : newValue; } } ``` Returning `oldValue` keeps the previous text and cursor, so the keystroke simply has no effect. Use rejection sparingly: users often cannot tell why their typing was ignored, which is another reason to keep the rule in the validator as well. ## Why the validator is still required Formatters work on single edits. They can keep out a `#` or a lowercase letter, but they cannot know that `AB1` is a plate someone is still typing while `AB12CDE` is finished. They also never see text that arrived through `controller.text` or a restored value. The **validator** sees the whole value at a meaningful moment, so it owns rules such as "2 to 8 characters, letters and digits" or a region-specific pattern, and it produces the message the user reads. The server remains the final authority on whether a plate is valid or already registered; client checks are for quick feedback. ## Pitfalls - **Order matters.** Filter first, then uppercase. Uppercasing some characters can change the string's length, and filtering to ASCII letters and digits first keeps the uppercase step length-preserving. - **Keep the selection consistent.** A formatter that inserts or removes characters, a space after the region code for example, must return a `selection` adjusted to the new text, or the cursor jumps. - **Masking is a design decision.** Whether to reformat input as the user types belongs to the design-system text-field spec; Flutter supplies only the mechanism, and a formatter that silently deletes characters can confuse users. - **Turn off correction for codes.** `autocorrect: false` and, on Android, `enableSuggestions: false` stop the keyboard from "fixing" a plate into a word.

  • Why must a formatter that inserts characters also adjust the selection?
    `formatEditUpdate` returns a complete `TextEditingValue`, and the field uses its `selection` as the new cursor position. If a formatter inserts a space but returns the incoming selection unchanged, the cursor lands one character too early and the next keystroke goes in the wrong place. Compute the new offset from the text you actually return.
  • Where does maxLength's limit apply relative to your own inputFormatters?
    `TextField` appends a `LengthLimitingTextInputFormatter` after the formatters you pass, so it counts the text your formatters produced. `maxLengthEnforcement` controls how strictly the limit applies while an input method is composing. The limit is still per edit; a value set through the controller is not truncated.

At a parking-permit office, the sign reading "capital letters only" is keyboardType: people may ignore it. The clerk who crosses out forbidden characters as each one is written is the formatter. The officer who reads the finished form and says whether it is a real plate is the validator; only that officer sees the whole thing.

saying these in an interview costs you the question

  • TextInputType.number guarantees that the field contains only digits.
  • TextCapitalization.characters uppercases whatever the user pastes.
  • With a filtering formatter in place, the plate validator is redundant.
  • inputFormatters also run when code assigns controller.text.
  • The order of the inputFormatters list does not matter.