Skip to main content

keyed_form_flutter 0.2.0 copy "keyed_form_flutter: ^0.2.0" to clipboard
keyed_form_flutter: ^0.2.0 copied to clipboard

Flutter bindings for keyed_form: a scope, field and list widgets with selective rebuilds, caret-safe text editing, and scroll-to-first-error anchors.

keyed_form_flutter #

The Flutter binding for keyed_form. Wrap an editor subtree in a KeyedForm to publish its KeyedFormController, then bind each field with KeyedFormField. Re-exports all of keyed_form (and thus keyed_form_core), so a screen needs one import: package:keyed_form_flutter/keyed_form_flutter.dart.

Installation #

dependencies:
  keyed_form_flutter: ^0.2.0
  keyed_form_schema: ^0.2.0

dev_dependencies:
  build_runner: ^2.15.0
  keyed_form_gen: ^0.2.0

Quick start #

Declare the form's shape as a schema, generate a data class and typed field references from it, then build the controller and widgets against those generated names.

// signup_schema.dart
@keyedSchema
library;

import 'package:keyed_form_schema/keyed_form_schema.dart';

part 'signup_schema.kfg.dart';

final _signupSchema = ks.object({
  'email': ks.string(error: .text('Email is required'))
      .email(error: .text('Enter a valid email')),
  'password': ks.string(error: .text('Password is required'))
      .min(8, error: .text('At least 8 characters')),
});
dart run build_runner build

This writes signup_schema.kfg.dart next to the schema file (commit it — it's real source) with class SignupSchema (email, password, .create(), .validate(), static .validateData) and abstract final class SignupFields (.email, .password).

import 'package:keyed_form_flutter/keyed_form_flutter.dart';

final form = KeyedFormController<SignupSchema>(
  initialValue: SignupSchema.create(),
  mode: KeyedFormMode.onTouched,
  resolver: SignupSchema.validateData,
);

// build():
KeyedForm<SignupSchema>(
  controller: form,
  child: Column(
    children: [
      KeyedFormField.text<SignupSchema>(
        field: SignupFields.email,
        builder: (context, state, controller) => TextField(
          controller: controller,
          onTapOutside: (_) => FocusScope.of(context).unfocus(),
          decoration: InputDecoration(
            labelText: 'Email',
            errorText: state.errorText,
          ),
        ),
      ),
      KeyedFormField.text<SignupSchema>(
        field: SignupFields.password,
        builder: (context, state, controller) => TextField(
          controller: controller,
          obscureText: true,
          onTapOutside: (_) => FocusScope.of(context).unfocus(),
          decoration: InputDecoration(
            labelText: 'Password',
            errorText: state.errorText,
          ),
        ),
      ),
      // handleSubmit needs a context from *inside* the tree KeyedForm builds
      // (see "context and handleSubmit" below) — Builder is the cheapest way
      // to get one when nothing else in the subtree already provides it.
      Builder(
        builder: (context) => ElevatedButton(
          onPressed: () => form.handleSubmit(context, (value) async {
            // value.email, value.password — already validated.
          }),
          child: const Text('Sign up'),
        ),
      ),
    ],
  ),
)

// dispose():
form.dispose();

In onTouched mode, blur starts validation and later edits to a blurred field revalidate it. Submit always validates the current draft. KeyedFormField automatically reports focus leaving its widget subtree; ordinary text fields need no Focus wrapper. onTapOutside only unfocuses the text input. For a control whose logical focus extends into an overlay, set autoDetectBlur: false and call state.onBlur() when that logical interaction ends. Enter does not count as blur.

Context and handleSubmit #

context for handleSubmit (and for KeyedForm.controllerOf / registryOf / translateErrorOf) must come from inside the KeyedForm subtree — the context a builder callback hands you (a Builder's, a KeyedFormField's, a KeyedFormSelector's), not the context of the widget that created the KeyedForm (that one sits above it in the tree, so an ancestor lookup from it fails).

Binding a dynamic list #

KeyedFieldList<InvoiceForm, LineItem>(
  field: InvoiceFields.lineItems,
  builder: (context, items, list) => Column(children: [
    for (final item in items)
      LineItemCard(key: ValueKey(item.clientId), id: item.clientId),
    AddButton(onPressed: () => list.append(LineItem.create())),
  ]),
)

InvoiceForm/InvoiceFields/LineItem here are the generated class and field references from your own schema, the same way SignupSchema / SignupFields are above. KeyedFieldList rebuilds only when the row set changes (add/remove/reorder); LineItemCard — one KeyedFormField per field inside it — handles edits within a row.

More #

Pieces #

Piece What it is
KeyedForm<Root> Publishes a KeyedFormController<Root> down the widget tree and owns a KeyedFieldRegistry internally — apps never construct one. controllerOf / registryOf / translateErrorOf read it back from a descendant context (see the context rule above)
KeyedFormField<Root, V> Binds one FieldRef to the ambient controller — see below
KeyedFieldList<Root, Item> Binds one list field and rebuilds only when the row set changes (add/remove/reorder); edits within a row are the job of the KeyedFormFields inside it
context.watchField(ref) / context.watchForm<Root>() / context.selectForm((f) => slice) Read a form slice in a widget's build() and rebuild that widget only when the slice changes. Non-reactive reads (form.field(x).value, form.isDirty in an event handler) are plain getters, no subscription
KeyedFormSelector<Root, T> The context.selectForm above wrapped in a widget, to scope the rebuild to a subtree, with a non-rebuilt child
KeyedFormBuilder<Root> Rebuilds on every controller change — the escape hatch for a widget that genuinely needs the whole state (a live inspector)
KeyedTextBinding A TextEditingController two-way bound to an external string value, keeping the caret and IME composing region stable as the value round-trips through the form controller. Design-system agnostic
form.handleSubmit(context, onValid, {onInvalid, onValidationUnavailable}) Validates, awaits async rules and runs onValid only when allowed; see below
KeyedFieldRegistry / KeyedFieldAnchor Maps FieldKeys to live field positions so a form can reveal and focus a mounted field

KeyedFormField rebuilds only when that field's value, visible error, isValidating, isFailedValidation, or isReadOnly changes. Rerun a configured rule explicitly with form.field(ref).validate(). KeyedFormField.text bundles a KeyedTextBinding for a String field. The builder output is anchored automatically (anchor: false opts out).

handleSubmit — value errors use onInvalid, whose default reveals the first visible error through the ambient registry. Blocking technical failures use onValidationUnavailable; the default also reveals their fields. An explicit callback replaces the default reveal for its outcome. See submit and scroll to error.

Scope #

Deliberately out of scope: any specific design system (buttons, inputs, layout) — this package binds state, not presentation.

1
likes
160
points
382
downloads

Documentation

API reference

Publisher

verified publisherv4g.space

Weekly Downloads

Flutter bindings for keyed_form: a scope, field and list widgets with selective rebuilds, caret-safe text editing, and scroll-to-first-error anchors.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #widget #form #validation #state-management

License

MIT (license)

Dependencies

flutter, keyed_form

More

Packages that depend on keyed_form_flutter