With Codegen
The recommended path: annotate your model, run build_runner, use the generated XFields and schema in your form.
1. Model + annotations
dart
// lib/models/user.dart
import 'package:validasi/validasi.dart';
import 'package:validasi_annotation/validasi_annotation.dart';
part 'user.g.dart';
@ValidateClass()
class User {
@Validate<String>([Required(), MinLength(2), MaxLength(100)])
final String name;
@Validate<String>([MinLength(5)])
final String email;
@Validate<int>([MinLength(1)])
final int age;
const User({required this.name, required this.email, required this.age});
}2. Generate
yaml
# build.yaml
targets:
$default:
builders:
validasi_gen:validasi:
options:
generateFields: true
generateSchema: true
generateValidateForm: truebash
dart run build_runner build --delete-conflicting-outputs3. Form widget
dart
import 'package:flutter/material.dart';
import 'package:validasi_ui/validasi_ui.dart';
import 'models/user.dart';
class UserFormPage extends StatelessWidget {
const UserFormPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User Form')),
body: ValidasiForm<User>(
schema: UserFields.schema,
builder: (context, submit) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Text field with auto TextEditingController
ValidasiTextField<User, String>(
field: UserFields.name,
builder: (context, state, controller) => TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Name',
errorText: state.errorText,
),
),
),
const SizedBox(height: 12),
ValidasiTextField<User, String>(
field: UserFields.email,
builder: (context, state, controller) => TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Email',
errorText: state.errorText,
),
),
),
const SizedBox(height: 12),
// Non-string field — parse manually
ValidasiFormField<User, int>(
field: UserFields.age,
builder: (context, state) => TextField(
onChanged: (raw) => state.onChanged(int.tryParse(raw)),
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Age',
errorText: state.errorText,
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: submit((user) {
// user is typed User, allocated by schema
saveUser(user);
}),
child: const Text('Submit'),
),
],
),
),
),
);
}
}4. Submit flow
submit((user) { ... })callscontroller.validate().- If valid, the schema's
allocate(controller)builds your model from registered field values. - Your callback receives the fully typed
Userinstance.
dart
// Submit with loading state
ElevatedButton(
onPressed: controller.isSubmitted && !controller.isValid
? null // disable after failed submit
: submit((user) {
setState(() => _loading = true);
saveUser(user).then((_) {
setState(() => _loading = false);
Navigator.pop(context);
});
}),
child: const Text('Submit'),
),5. Accessing the controller
dart
final controller = ValidasiForm.of<User>(context);
// Check state
print(controller.isDirty);
print(controller.isTouched);
print(controller.isValid);
// Programmatic control
controller.validate();
controller.setValue(UserFields.email, 'new@email.com');
controller.reset();
// Read values
final email = controller.getValue(UserFields.email);
final allValues = controller.getValues();6. Async validation with codegen
dart
Future<bool> _emailAvailable(String? email) async {
if (email == null) return false;
final taken = await checkEmailTaken(email);
return !taken;
}
@ValidateClass()
class User {
@Validate<String>([Required(), MinLength(5), AsyncInline(_emailAvailable)])
final String email;
const User({required this.email});
}The generator wires AsyncInline into the field's validateAsync(). The form controller automatically runs async validation when the field is validated. The builder receives state.isValidating to show a spinner:
dart
ValidasiTextField<User, String>(
field: UserFields.email,
builder: (context, state, controller) => TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Email',
errorText: state.errorText,
suffixIcon: state.isValidating
? const SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
),
),
),7. Cross-field with codegen
dart
@ValidateClass()
class SignUpForm {
final String password;
final String confirmPassword;
@RefineFn(dependsOn: ['password', 'confirmPassword'])
void passwordsMatch(FailFn fail, {String? password, String? confirmPassword}) {
if (password != null && confirmPassword != null && password != confirmPassword) {
fail(message: 'Passwords do not match', path: ['confirmPassword']);
}
}
const SignUpForm({required this.password, required this.confirmPassword});
}Cross-field errors appear alongside per-field errors in the form controller. The generateValidateForm: true option enables validateForm_SignUpForm(controller) which reads values from the controller and runs refines.
