This short path creates a validated login form. The complete example includes the extracted field widget and submit handling.
1. Install#
flutter pub add keyed_form_flutter keyed_form_schema
flutter pub add -d keyed_form_gen build_runner
2. Define the schema#
@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#
dart run build_runner build -d
Commit the generated *.kfg.dart file. Generation happens during development, never at runtime.
4. Connect the form#
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.