keyed_form

Quickstart

Define a schema, generate typed fields, then connect them to a Flutter form.

Comparison copy: this is the previous documentation and may contain outdated guidance. Read the reorganized guides

This short path creates a validated login form. The complete example includes the extracted field widget and submit handling.

1. Install#

sh
flutter pub add keyed_form_flutter keyed_form_schema
flutter pub add -d keyed_form_gen build_runner

2. Define the schema#

dart
@keyedSchema
library;

import 'package:keyed_form_schema/keyed_form_schema.dart';

part 'login_schema.kfg.dart';

final _loginSchema = ks.object({
  'email': ks.string().email(error: .text('Enter a valid email')),
  'password': ks.string().min(8, error: .text('Use at least 8 characters')),
});

3. Generate the typed model#

sh
dart run build_runner build -d

Commit the generated *.kfg.dart file. Generation happens during development, never at runtime.

4. Connect the form#

dart
final form = KeyedFormController<LoginSchema>(
  initialValue: LoginSchema.create(),
  mode: KeyedFormMode.onChange,
  resolver: LoginSchema.validateData,
);

Wrap the fields in KeyedForm<LoginSchema> and use KeyedFormField.text with LoginFields.email and LoginFields.password. Dispose the controller with the owning state, and submit through form.handleSubmit(context, onValid).

@keyedSchema
library;

import 'package:keyed_form_schema/keyed_form_schema.dart';

part 'login_schema.kfg.dart';

final _loginSchema = ks.object({
  'email': ks
      .string(error: .text('Enter your email'))
      .email(error: .text('That does not look like an email')),
  'password': ks
      .string(error: .text('Enter your password'))
      .min(8, error: .text('At least 8 characters')),
});
import 'package:flutter/material.dart';
import 'package:keyed_form_flutter/keyed_form_flutter.dart';

import 'login_schema.dart';

part 'login_text_field.dart';

class LoginForm extends StatefulWidget {
  const LoginForm({super.key, this.onSignedIn});

  final void Function(LoginSchema value)? onSignedIn;

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final form = KeyedFormController<LoginSchema>(
    initialValue: LoginSchema.create(),
    mode: KeyedFormMode.onTouched,
    resolver: LoginSchema.validateData,
  );

  @override
  void dispose() {
    form.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return KeyedForm<LoginSchema>(
      controller: form,
      child: Column(
        children: [
          _LoginTextField(field: LoginFields.email, label: 'Email'),
          const SizedBox(height: 16),
          _LoginTextField(
            field: LoginFields.password,
            label: 'Password',
            obscureText: true,
          ),
          const SizedBox(height: 16),
          // handleSubmit needs a context below KeyedForm.
          Builder(
            builder: (context) => FilledButton(
              onPressed: () => form.handleSubmit(
                context,
                (value) => widget.onSignedIn?.call(value),
              ),
              child: const Text('Sign in'),
            ),
          ),
        ],
      ),
    );
  }
}
part of 'login_form.dart';

class _LoginTextField extends StatelessWidget {
  const _LoginTextField({
    required this.field,
    required this.label,
    this.obscureText = false,
  });

  final FieldRef<LoginSchema, String> field;
  final String label;
  final bool obscureText;

  @override
  Widget build(BuildContext context) {
    return KeyedFormField.text<LoginSchema>(
      field: field,
      builder: (context, f, controller) => TextField(
        controller: controller,
        obscureText: obscureText,
        onTapOutside: (_) => f.onBlur(),
        decoration: InputDecoration(
          labelText: label,
          errorText: f.errorText,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }
}

See installation for requirements and generator watch mode.