skip to content

Visual Design Kits

Flutter's Material 3 and Cupertino widget libraries, ThemeData and color schemes, text and box styling, and Form-based text input. Interviewers treat these as everyday UI fluency.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

27

In Flutter, how do a Form, its GlobalKey<FormState> and TextFormField validators work together when the user taps Submit?

level: juniorimportance: must knowfreq 72%

answer

  1. one key, one FormState
  2. fields register with the nearest Form
  3. validator returns a message or null
  4. validate() returns bool and rebuilds errors
  5. save() fires every onSaved

basics

~20 s

A Form keyed with a GlobalKey<FormState> tracks its TextFormField descendants. On submit, validate() runs every validator (null means valid, a string becomes the error), returns true only if all pass, and save() then calls each onSaved.

solid answer

~30 s

`Form` is a stateful container: every `FormField` below it, `TextFormField` included, registers its `FormFieldState` with the enclosing `FormState`. You reach that state through a `GlobalKey<FormState>` created once as a field of your `State`, and on submit call `_formKey.currentState!.validate()`. It runs each field's `validator`, a synchronous `String? Function(String?)` where `null` means valid and a string becomes that field's error text, rebuilds the fields so the errors appear, and returns `true` only when none failed. `validate()` saves nothing: call `save()` afterwards to fire every `onSaved`, or read your controllers. `reset()` puts each field back to its initial value and clears its error.

code

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

class VehicleForm extends StatefulWidget {
  const VehicleForm({super.key, required this.onSubmit});

  final void Function(String plate, String owner) onSubmit;

  @override
  State<VehicleForm> createState() => _VehicleFormState();
}

class _VehicleFormState extends State<VehicleForm> {
  final _formKey = GlobalKey<FormState>();
  final _plateFormat = RegExp(r'^[A-Z0-9]{2,8}$');
  String _plate = '';
  String _owner = '';

  void _submit() {
    final form = _formKey.currentState!;
    if (!form.validate()) return;
    form.save();
    widget.onSubmit(_plate, _owner);
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(labelText: 'Plate number'),
            validator: (value) {
              final plate = value?.trim() ?? '';
              if (plate.isEmpty) return 'Enter the plate number';
              if (!_plateFormat.hasMatch(plate)) {
                return 'Use 2-8 capital letters or digits';
              }
              return null;
            },
            onSaved: (value) => _plate = value?.trim() ?? '',
          ),
          TextFormField(
            decoration: const InputDecoration(labelText: 'Owner name'),
            validator: (value) =>
                (value == null || value.trim().isEmpty) ? 'Enter the owner name' : null,
            onSaved: (value) => _owner = value?.trim() ?? '',
          ),
          FilledButton(onPressed: _submit, child: const Text('Register')),
        ],
      ),
    );
  }
}

go deeper

for a junior

Recall the contract: a GlobalKey<FormState>, a validator that returns null for valid or a message for invalid, validate() returning a bool, then save() for onSaved.

for a middle

Explain registration: fields join the nearest FormState as they build, validate() runs every validator and rebuilds, and reset() restores initial values rather than emptying fields.

for a senior

Show where forms go wrong in production: a key created in build, validators with side effects, unmounted fields that never validate, and submit handlers that save before validating.

for a principal

Discuss when Form plus per-field validators stops scaling and validation belongs in a model or state object that the widgets only display.

## The three pieces Flutter's form support is built from three cooperating parts: - **`Form`** is a `StatefulWidget` whose state, **`FormState`**, keeps a set of every `FormFieldState` registered beneath it. It adds no layout of its own; you put a `Column` or a `ListView` inside it. - **`FormField<T>`** is the generic field: it holds a value of type `T`, an optional `validator`, an `onSaved` callback and an `initialValue`. **`TextFormField`** is a `FormField<String>` that wraps a Material `TextField` and passes the field's current error into the `InputDecoration`. - **`GlobalKey<FormState>`** is how code *outside* the form's subtree (typically the submit button's handler in the same `build` method) reaches the `FormState`. Create it once, as a field of your `State`; a key created inside `build()` is a new key on every rebuild, so the form's state and everything typed into it are thrown away. A field registers with the nearest enclosing `Form` each time it builds and unregisters when it is deactivated, so the set always matches the fields that are currently mounted. ## What validate() actually does When the user taps Submit and you call `_formKey.currentState!.validate()`: 1. The form marks itself as interacted with and schedules a rebuild. 2. It walks every registered field and calls that field's `validate()`, which runs the `validator` with the field's current value (a `forceErrorText`, if one is set, is used instead of the validator). 3. Each result is stored as the field's `errorText`. A `null` result means valid; **any** non-null string, even `''`, means invalid. 4. The fields rebuild; `TextFormField` copies the error into its decoration, so the message appears under the input. 5. `validate()` returns `true` only if no field ended up with an error. The validator's type is `FormFieldValidator<T>`, which is `String? Function(T? value)`. It is synchronous: it cannot `await` anything, and under some autovalidate modes it runs on every rebuild, so it must be cheap and free of side effects. ## Saving, resetting and the other FormState methods | Method | What it does | |---|---| | `validate()` | Runs every validator, shows the errors, returns `bool` | | `validateGranularly()` | Same, but returns the `Set<FormFieldState<Object?>>` of invalid fields | | `save()` | Calls every field's `onSaved(value)`; runs no validation | | `reset()` | Restores each field's initial value, clears errors, fires `Form.onChanged` | | `clearError()` | Clears every error but keeps the values (Flutter 3.44+) | The usual submit handler is therefore *validate, then save, then use the values*. `onSaved` is a convenient place to copy each value into a model object; if you already hold `TextEditingController`s you can skip `save()` and read `controller.text` instead. `reset()` is subtler than "clear the form". Each field goes back to its `initialValue`; a `TextFormField` given a controller goes back to the text the controller held when the field was first built. The field also forgets that the user touched it, so autovalidation stops until the next edit. ## Fields that depend on each other Whenever any field's value changes, `FormState` notifies `Form.onChanged` and rebuilds its scope, which rebuilds every registered field. The framework does this deliberately, for **interdependent fields**: a validator may read another field's value, for instance through that field's `TextEditingController`, and it will be re-run on the next validation pass with the fresh value. Typical cases: - a "confirm plate number" field whose validator compares its value with the plate field's controller; - a registration expiry date that must fall after the issue date entered above it; - a field that is required only when a checkbox elsewhere in the form is ticked. Because all fields rebuild on every change, validators must stay cheap: no parsing of large data, no I/O, just checks on values already in memory. ## A vehicle-registration example The code example below registers a vehicle: the plate number must be 2 to 8 capital letters or digits, and the owner's name must not be empty. The button calls `validate()`; only when it returns `true` does the handler call `save()` and pass the saved values on. ## Mistakes interviewers listen for - Returning `true`/`false` from a validator, or returning `''` for "valid": the contract is `null` for valid. - Expecting `validate()` to call `onSaved`; it never does. - Calling `Form.of(context)` with the `context` of the widget that *builds* the `Form`: that context sits above the form, so the lookup fails. Use the `GlobalKey`, or a `Builder` below the form. - Putting a network call in a validator (it cannot be awaited and may run many times). - Creating the `GlobalKey<FormState>` inside `build()`.

  • Why reach the FormState through a GlobalKey instead of Form.of(context) in the submit handler?
    `Form.of(context)` looks for the nearest `Form` *above* the given context. The submit handler usually runs in the same `build` method that creates the `Form`, so its `context` sits above the form and the lookup fails. A `GlobalKey<FormState>` held as a `State` field reaches the form from anywhere; `Form.of` works only from a context below the form, such as inside a `Builder`.
  • What does validateGranularly() give you that validate() does not?
    `validateGranularly()` runs the same validation but returns a `Set<FormFieldState<Object?>>` of the fields that failed instead of a `bool`. That lets you act on specific fields, for example scrolling the first invalid one into view with `Scrollable.ensureVisible(field.context)`. Like `validate()`, it marks the form as interacted with and rebuilds it.
  • After FormState.reset(), what does a TextFormField created with initialValue: 'AB123' show?
    It shows `AB123` again with no error. `reset()` restores every field's initial value, clears its error and its has-interacted flag, and calls `Form.onChanged`. `TextFormField` also writes the initial text back into its controller and calls its own `onChanged`. Only a form whose `autovalidateMode` is `always` revalidates straight away.

saying these in an interview costs you the question

  • A validator returns true when the value is valid.
  • Returning an empty string from a validator marks the field as valid.
  • validate() also calls onSaved, so save() is never needed.
  • Creating the GlobalKey<FormState> inside build() is harmless.
  • A validator can await a server check before returning.
open as a page

In Flutter, what do MaterialApp and Scaffold each provide, and why does Scaffold.of(context) sometimes fail to find the Scaffold?

level: juniorimportance: must knowfreq 70%

basics

~20 s

MaterialApp sets up app-wide services: the Navigator, the Theme, Material localizations and a root ScaffoldMessenger. Scaffold lays out one screen's Material slots. Scaffold.of fails when its context sits above the Scaffold, such as the widget that builds it.

open as a page

In Flutter, how do you keep a long Text from spilling out of its space, and what do maxLines, overflow and softWrap each control?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Give the Text a bounded width, then limit it with maxLines and choose how the cut is drawn with overflow (clip, fade, ellipsis or visible). softWrap decides whether text may break onto new lines at all; it defaults to true.

open as a page

In Flutter, how do you style widgets from the app theme instead of hardcoding colors and text styles?

level: juniorimportance: must knowfreq 68%

basics

~10 s

Define one ThemeData, usually with a ColorScheme and a TextTheme, pass it to MaterialApp's theme, then read Theme.of(context).colorScheme and .textTheme in widgets instead of writing Colors.blue or a literal TextStyle.

open as a page

In Flutter, where should a TextFormField's TextEditingController be created and disposed, and what breaks if build() creates it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Create a TextEditingController once per State, as a field initializer or in initState, and call its dispose() in dispose(). Created inside build(), every rebuild hands the field a fresh empty controller, losing the typed text, and the old ones are never disposed.

open as a page

In Flutter, how do showDialog and showModalBottomSheet return a result to the caller, and which of their defaults most often surprise developers?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both return a Future<T?> that completes with the value passed to Navigator.pop, or null when the user dismisses them. Surprising defaults: barrierDismissible is true, and a modal sheet is capped at 9/16 of the height unless isScrollControlled is true.

open as a page

In Flutter, what is the difference between Container and DecoratedBox, and when would you choose each?

level: middleimportance: must knowfreq 55%

basics

~20 s

Container is a convenience widget that composes smaller widgets such as Padding, ColoredBox, DecoratedBox, ConstrainedBox, Align and Transform. DecoratedBox only paints a Decoration. Choose DecoratedBox or ColoredBox when you need just the paint, Container when several of its features combine.

open as a page

In Flutter, how do ColorScheme.fromSeed, darkTheme and themeMode work together to give an app matching light and dark themes?

level: middleimportance: must knowfreq 55%

basics

~20 s

ColorScheme.fromSeed derives every Material 3 color role from one seed color for a given brightness. Build a light and a dark ThemeData from the same seed, pass them as theme and darkTheme, and themeMode (default ThemeMode.system) picks one.

open as a page

In Flutter, what do CupertinoApp, CupertinoPageScaffold and CupertinoNavigationBar give you compared with MaterialApp, Scaffold and AppBar?

level: juniorimportance: should knowfreq 45%

basics

~20 s

They are Flutter's own iOS-style counterparts: CupertinoApp sets up navigation, a CupertinoThemeData and iOS page routes; CupertinoPageScaffold lays out a page under an optional navigation bar; CupertinoNavigationBar draws the iOS bar with leading, middle and trailing slots.

open as a page

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%

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.

open as a page

In Flutter's Material 3, how do FilledButton, FilledButton.tonal, ElevatedButton, OutlinedButton and TextButton differ, and when do you use each?

level: juniorimportance: should knowfreq 45%

basics

~20 s

They are the same kind of button at different emphasis: FilledButton for the key final action, FilledButton.tonal a step lower, ElevatedButton when a lifted surface is needed, OutlinedButton for medium emphasis and TextButton for the least, such as dialog actions.

open as a page

In Flutter, what do constructors like Switch.adaptive, Slider.adaptive and CircularProgressIndicator.adaptive do, and how do they decide which look to render?

level: middleimportance: should knowfreq 42%

basics

~10 s

They render the Cupertino look when Theme.of(context).platform is iOS or macOS and the Material widget elsewhere. The platform comes from ThemeData.platform, which defaults to defaultTargetPlatform, and some Material-only parameters are ignored on Apple platforms.

open as a page

In a Flutter CupertinoPageScaffold, why can the first row of content hide under the CupertinoNavigationBar, and how do you fix it?

level: middleimportance: should knowfreq 30%

basics

~20 s

The default Cupertino bar background is translucent, so the scaffold lets the child draw behind it and only reports the overlap as MediaQuery top padding. Content that ignores that padding, such as a Column or a ListView with explicit padding, starts under the bar.

open as a page

In Flutter, how does each AutovalidateMode value change when Form and TextFormField validators run, and which suits a registration form?

level: middleimportance: should knowfreq 48%

basics

~20 s

AutovalidateMode decides when validators run without an explicit validate(): disabled (the default) waits for it; always runs on every build; onUserInteraction after an edit; onUnfocus when a field loses focus; onUserInteractionIfError re-checks only fields already showing an error.

open as a page

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

level: middleimportance: should knowfreq 38%

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.

open as a page

In Flutter's Material 3, how do NavigationBar and NavigationRail manage the selected destination, and when do you use each instead of BottomNavigationBar?

level: middleimportance: should knowfreq 42%

basics

~10 s

Neither widget stores the selection: you pass selectedIndex and update it in onDestinationSelected. NavigationBar is the Material 3 bottom bar that supersedes BottomNavigationBar on phones; NavigationRail is the vertical version for wider screens.

open as a page

In Flutter, how do you show a SnackBar through ScaffoldMessenger, and how does a SnackBar with an action behave by default since Flutter 3.38?

level: middleimportance: should knowfreq 48%

basics

~10 s

Call ScaffoldMessenger.of(context).showSnackBar(SnackBar(...)); the messenger from MaterialApp shows it on the current Scaffold and queues extras. Since Flutter 3.38 a SnackBar with an action stays until dismissed unless persist is false.

open as a page

In Flutter, how does DefaultTextStyle supply styles to Text widgets, and why does text outside a Material widget show red with a yellow double underline?

level: middleimportance: should knowfreq 38%

basics

~20 s

DefaultTextStyle is an inherited widget whose style every descendant Text merges under its own style. Material and Scaffold set it from the theme's bodyMedium; text with no such ancestor under MaterialApp gets a deliberate red, double-yellow-underlined warning style.

open as a page

In Flutter, how do you render a price line mixing a struck-through old price with a bold new price, and why prefer Text.rich over RichText?

level: middleimportance: should knowfreq 45%

basics

~10 s

Build a TextSpan tree, each child with its own TextStyle, and pass it to Text.rich. Text.rich merges the ambient DefaultTextStyle, text scaling and selection support, while RichText uses only what you pass it.

open as a page

In Flutter, how do you add custom design tokens to ThemeData with ThemeExtension, and why must you implement copyWith and lerp?

level: middleimportance: should knowfreq 42%

basics

~10 s

Subclass ThemeExtension<T> with your token fields, implement copyWith and lerp, register instances in ThemeData.extensions for each theme, and read them with Theme.of(context).extension<T>(). lerp lets the tokens animate when the theme changes.

open as a page

In a Flutter vehicle-registration form, how do you show a server's "plate already registered" error on a TextFormField when validators are synchronous?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Validate locally, submit, and when the server rejects the plate, store its message in State and pass it as the field's forceErrorText (Flutter 3.24+); clear it when the user edits. Never put the network call inside a validator.

open as a page

Flutter 3.47 offers Material as the standalone material_ui package; how do you migrate an app off package:flutter/material.dart, and what can break?

level: seniorimportance: should knowfreq 20%

basics

~10 s

Run dart fix --apply --code=migrate_design_widgets and add material_ui (and cupertino_ui if used). Dependencies still importing flutter/material.dart can be bridged with MaterialUiCompatibilityBridge for context lookups, but not where their public APIs expose Material types.

open as a page

In Flutter, how would you build a coupon card with a gradient, rounded corners, an edge-to-edge image and a dashed border, and where do BoxDecoration's limits force other tools?

level: seniorimportance: should knowfreq 32%

basics

~20 s

BoxDecoration supplies the gradient, radius and shadow, but it never clips the child and cannot dash a border. Clip the image with ClipRRect or clipBehavior (Card defaults to Clip.none), and draw the dashed outline with a custom painter or a package.

open as a page

In a Flutter app, one screen's FilledButton ignores the app's filledButtonTheme; what style resolution order explains it, and how do local Theme overrides fit in?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Material buttons resolve each property from the widget's own style first, then the nearest component theme (a FilledButtonTheme widget, else ThemeData.filledButtonTheme), then built-in defaults from the ColorScheme. A local style, a nearer theme or a fresh ThemeData explains the mismatch.

open as a page

In a white-label Flutter loyalty app rebranded per retailer with dark mode, how would you structure ThemeData so no screen hardcodes a brand value?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Model each retailer as a small brand config (seed, optional exact colors, fonts, token values), and build light and dark ThemeData from it in one factory: fromSeed schemes, a TextTheme, component themes and ThemeExtension tokens. Screens only read Theme.of(context).

open as a page

For an iOS-first Flutter wine-cellar app that must feel native on iPhone and also ship on Android, how do you choose between one brand look and platform-adaptive UI?

level: principalimportance: should knowfreq 35%

basics

~20 s

Decide by what users notice and what the team can maintain: keep brand surfaces shared, adapt the controls and navigation conventions iPhone users expect (switches, dialogs, back swipe, pickers), and avoid two full widget trees unless the product truly needs them.

open as a page

In Flutter 3.47, what is the cupertino_ui package, and how does it relate to package:flutter/cupertino.dart?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

cupertino_ui is the standalone pub package of Flutter's Cupertino library. The in-SDK package:flutter/cupertino.dart has been frozen since Flutter 3.44; cupertino_ui 1.0 started from that frozen code and now evolves on its own release cycle, needing Flutter 3.47 or later.

open as a page