Without Codegen
You can use validasi_ui without annotations or build_runner. Define ValidasiField instances manually and register them with the form controller. This is useful for quick prototypes, dynamic forms, or when you prefer explicit control over generated code.
Manual field definition
dart
import 'package:validasi/validasi.dart';
import 'package:validasi_ui/validasi_ui.dart';
class User {
final String name;
final String email;
const User({required this.name, required this.email});
}
// Define fields manually
final nameField = ValidasiField<User, String>(
name: 'name',
extract: (user) => user.name,
validate: Validasi.string([Rules.string.minLength(2)]).validate,
);
final emailField = ValidasiField<User, String>(
name: 'email',
extract: (user) => user.email,
validate: Validasi.string([Rules.string.minLength(5)]).validate,
);ValidasiField<T, V> takes three required parameters:
| Parameter | Description |
|---|---|
name | Unique field identifier (for error paths) |
extract | V? Function(T) — reads the field value from a model instance |
validate | ValidasiResult<V> Function(V?) — the validation pipeline |
Defining a schema
dart
final userSchema = ValidasiSchema<User>(
allocate: (reader) => User(
name: reader.getValue(nameField) as String,
email: reader.getValue(emailField) as String,
),
);Form widget
dart
ValidasiForm<User>(
schema: userSchema,
builder: (context, submit) => Column(
children: [
ValidasiFormField<User, String>(
field: nameField,
builder: (context, state) => TextField(
onChanged: state.onChanged,
decoration: InputDecoration(
labelText: 'Name',
errorText: state.errorText,
),
),
),
ValidasiFormField<User, String>(
field: emailField,
builder: (context, state) => TextField(
onChanged: state.onChanged,
decoration: InputDecoration(
labelText: 'Email',
errorText: state.errorText,
),
),
),
ElevatedButton(
onPressed: submit((user) {
print('Saved: ${user.name}');
}),
child: const Text('Submit'),
),
],
),
);Using ValidasiTextField manually
dart
ValidasiTextField<User, String>(
field: nameField,
builder: (context, state, controller) => TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Name',
errorText: state.errorText,
),
),
),ValidasiTextField automatically handles TextEditingController creation, form-value sync, and disposal — just pass controller to your TextField.
Manual ValidasiField with async validation
dart
final emailField = ValidasiField<User, String>(
name: 'email',
extract: (user) => user.email,
validate: Validasi.string([Rules.string.minLength(5)]).validate,
validateAsync: (value) async {
// Runs after sync validation
if (value != null && value.isNotEmpty) {
final available = await checkEmailAvailability(value);
if (!available) {
return ValidasiResult.error(
ValidationError(rule: 'email', message: 'Email already taken'),
);
}
}
return ValidasiResult.success(value);
},
);Pass validateAsync to define async validation for a manual field. The form controller runs it when validateAsync() is called.
When to skip codegen
- Prototyping — iterate on form structure without running
build_runnerevery change. - Dynamic forms — form fields are determined at runtime (e.g. from a JSON schema).
- Small apps — a handful of fields where the overhead of annotations isn't worth it.
- Non-Dart models — the model class is defined in another language or generated by an external tool.
When to use codegen
- Type safety — generated
XFields<V>ensuresfieldandvaluetypes match. - Ergonomics —
UserFields.nameinstead of manually tracking const field objects. - Maintainability — add a field to the model and
build_runneremits everything. - Cross-field —
@RefineFnis only available through codegen. - Refactoring — rename a field and the generated code follows.
