Complete API documentation for the FieldBuilderContext class introduced in ChampionForms v0.6.0.
FieldBuilderContext is the cornerstone of the simplified custom field API in v0.6.0. It bundles all parameters needed by field builders into a single context object, reducing the builder signature from 6 parameters to 1.
Location: lib/models/field_builder_context.dart
Purpose:
- Simplify field builder signatures
- Provide convenient helper methods
- Support lazy resource initialization
- Enable theme-aware field development
import 'package:flutter/material.dart';
import 'package:championforms/championforms.dart' as form;
class MyCustomField extends form.StatefulFieldWidget<form.Field> {
const MyCustomField({required super.context});
@override
Widget buildWithTheme(
BuildContext buildContext,
FormTheme theme,
form.FieldBuilderContext ctx,
) {
// Get current value
final value = ctx.getValue<String>() ?? '';
// Access field properties
final title = ctx.field.title;
// Use theme-aware colors
final borderColor = ctx.colors.borderColor;
return TextField(
controller: ctx.getTextController(),
focusNode: ctx.getFocusNode(),
decoration: InputDecoration(
labelText: title,
border: OutlineInputBorder(
borderSide: BorderSide(color: borderColor),
),
),
onChanged: (newValue) {
ctx.setValue(newValue);
},
);
}
}Before v0.6.0 (6 parameters):
typedef FormFieldBuilder = Widget Function(
FormController controller,
Field field,
FormTheme theme,
List<Validator> validators,
FieldCallbacks callbacks,
FieldState state,
);v0.6.0+ (single context parameter):
typedef FormFieldBuilder = Widget Function(FieldBuilderContext context);All properties are final and initialized via the constructor.
final FormController controller;Description: The FormController managing this field's state.
Use Case: Advanced operations that require direct controller access.
Example:
// Access other fields' values
final otherValue = ctx.controller.getFieldValue('other_field_id');
// Programmatically set focus
ctx.controller.setFieldFocus(ctx.field.id, true);
// Trigger validation manually
ctx.controller.validateField(ctx.field.id);Best Practice: Prefer using context convenience methods over direct controller access when possible.
final Field field;Description: The field definition for this field (e.g., TextField, OptionSelect).
Properties Available:
field.id- Unique field identifierfield.title- Display titlefield.description- Help textfield.validators- List of validatorsfield.defaultValue- Default valuefield.validateLive- Validate on blur flagfield.onChange- Change callbackfield.onSubmit- Submit callback
Example:
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
return Column(
children: [
if (ctx.field.title != null)
Text(ctx.field.title!),
if (ctx.field.description != null)
Text(ctx.field.description!, style: theme.hintTextStyle),
// Field UI
],
);
}final FormTheme theme;Description: The resolved theme for this field after cascading.
Cascade Order: Default Theme → Global Theme → Form Theme → Field Theme
Properties Available:
theme.normalColors- Colors for normal statetheme.activeColors- Colors for focused statetheme.errorColors- Colors for error statetheme.disabledColors- Colors for disabled statetheme.selectedColors- Colors for selected statetheme.titleTextStyle- Title text stylingtheme.descriptionTextStyle- Description text stylingtheme.hintTextStyle- Hint text stylingtheme.chipTextStyle- Chip text stylingtheme.inputDecoration- InputDecoration builder
Example:
Text(
ctx.field.title!,
style: ctx.theme.titleTextStyle?.copyWith(fontWeight: FontWeight.bold),
)final FieldState state;Description: The current state of this field.
Possible Values:
FieldState.normal- Default stateFieldState.active- Field is focusedFieldState.error- Validation error existsFieldState.disabled- Field is disabledFieldState.selected- Option is selected (multiselect)
Use Case: Conditional rendering based on field state.
Example:
Container(
decoration: BoxDecoration(
border: Border.all(
color: ctx.state == FieldState.error
? Colors.red
: ctx.colors.borderColor,
width: ctx.state == FieldState.active ? 2 : 1,
),
),
child: // Field content
)final FieldColorScheme colors;Description: The color scheme for the current field state (automatically selected from theme based on state).
Properties Available:
colors.backgroundColor- Background colorcolors.borderColor- Border colorcolors.textColor- Text colorcolors.iconColor- Icon colorcolors.hintColor- Hint text colorcolors.backgroundGradient- Optional gradient
Use Case: Apply theme-aware colors without manual state checking.
Example:
TextField(
decoration: InputDecoration(
fillColor: ctx.colors.backgroundColor,
border: OutlineInputBorder(
borderSide: BorderSide(color: ctx.colors.borderColor),
),
hintStyle: TextStyle(color: ctx.colors.hintColor),
),
)T? getValue<T>()Description: Gets the current value for this field with type safety.
Returns: The field's value cast to type T, the default value, or null.
Type Parameters:
String- Text fieldsList<FieldOption>- Multiselect fieldsList<FileModel>- File upload fieldsint,double- Numeric fields- Any custom type
Example:
// Text field
final name = ctx.getValue<String>() ?? '';
// Multiselect
final options = ctx.getValue<List<form.FieldOption>>() ?? [];
// File upload
final files = ctx.getValue<List<form.FileModel>>() ?? [];
// Custom type
final rating = ctx.getValue<int>() ?? 0;See Also: setValue
void setValue<T>(T value, {bool noNotify = false})Description: Sets the value for this field.
Parameters:
value- The new value to setnoNotify- Iftrue, suppresses listener notification (default:false)
Triggers:
- Controller listeners (unless
noNotifyis true) - onChange callback (unless
noNotifyis true) - Widget rebuilds
Example:
// Simple update
ctx.setValue('new value');
// Silent update (no notification)
ctx.setValue('new value', noNotify: true);
// Update multiselect
final updated = List<form.FieldOption>.from(selectedOptions)..add(newOption);
ctx.setValue(updated);
// Update numeric value
ctx.setValue<int>(5);See Also: getValue
void addError(String reason)Description: Adds a validation error for this field.
Parameters:
reason- The error message to display
Behavior:
- Does NOT clear existing errors
- Adds error to controller's error list
- Triggers error state for field
- Error displayed by Form widget
Example:
// Add single error
ctx.addError('Invalid email format');
// Add multiple errors
ctx.clearErrors(); // Clear first
ctx.addError('Field is required');
ctx.addError('Must be at least 8 characters');
// Conditional error
if (value.length < 8) {
ctx.addError('Password must be at least 8 characters');
}See Also: clearErrors
void clearErrors()Description: Clears all validation errors for this field.
Behavior:
- Removes all errors associated with this field's ID
- Resets field state to normal (if no other errors exist)
- Does NOT trigger validation
Use Case: Clear errors before re-validating or when value changes.
Example:
// Clear before validating
ctx.clearErrors();
if (isInvalid) {
ctx.addError('Validation failed');
}
// Clear on value change
@override
void onValueChanged(dynamic oldValue, dynamic newValue) {
ctx.clearErrors(); // Remove previous errors
}See Also: addError
bool get hasFocusDescription: Returns whether this field is currently focused.
Returns: true if focused, false otherwise.
Use Case: Conditional UI based on focus state.
Example:
// Show autocomplete only when focused
if (ctx.hasFocus) {
return AutocompleteOverlay(/* ... */);
}
// Change border color on focus
border: Border.all(
color: ctx.hasFocus
? ctx.theme.activeColors.borderColor
: ctx.colors.borderColor,
),
// Trigger validation on blur
@override
void onFocusChanged(bool isFocused) {
if (!isFocused && ctx.field.validateLive) {
ctx.controller.validateField(ctx.field.id);
}
}See Also: getFocusNode
TextEditingController getTextController()Description: Gets or creates a TextEditingController for this field (lazy initialization).
Behavior:
- Creates controller on first call
- Returns cached instance on subsequent calls
- Automatically registered with FormController
- Automatically disposed by FormController
Initialization:
- Initial text set from field's current value
- Synced with field value in controller
Important: Do NOT manually dispose the returned controller.
Example:
final textController = ctx.getTextController();
return TextField(
controller: textController,
decoration: InputDecoration(
labelText: ctx.field.title,
),
onChanged: (value) {
// Controller value automatically updated
// Optionally trigger callbacks:
ctx.setValue(value); // Updates controller state
},
);Value Synchronization Pattern:
When fields can be updated programmatically (via controller.updateTextFieldValue() or setValue()), ensure the TextEditingController stays synchronized:
final textController = ctx.getTextController();
final value = ctx.getValue<String>() ?? '';
// Ensure controller has current value (prevents stale values)
if (textController.text != value) {
textController.text = value;
}
return TextField(controller: textController);Why This Matters: Without synchronization, external updates (e.g., programmatic field updates, form resets) won't reflect in the TextField until the user types. This pattern ensures the visual state matches the controller state.
Performance Note: Only allocated when actually needed - don't call unless you need text editing.
See Also: getFocusNode
FocusNode getFocusNode()Description: Gets or creates a FocusNode for this field (lazy initialization).
Behavior:
- Creates focus node on first call
- Returns cached instance on subsequent calls
- Automatically registered with FormController
- Automatically disposed by FormController
Controller Integration:
- Enables focus state tracking via
hasFocus - Enables automatic validation on focus loss
- Enables programmatic focus control
Important: Do NOT manually dispose the returned focus node.
Example:
final focusNode = ctx.getFocusNode();
return TextField(
focusNode: focusNode,
onFocusChange: (focused) {
if (!focused && ctx.field.validateLive) {
ctx.controller.validateField(ctx.field.id);
}
},
);Performance Note: Only allocated when actually needed - don't call unless you need focus management.
See Also: getTextController, hasFocus
When working with OptionSelect fields (checkboxes, switches, chip selects, radio buttons), the context provides additional helper methods for managing selected options.
bool isOptionSelected(String optionValue)Description: Checks if a specific option is currently selected in a multiselect field.
Parameters:
optionValue- The value of the option to check (matchesFieldOption.value)
Returns: true if the option is selected, false otherwise.
Use Case: Determining visual state for checkboxes, switches, and chip selects.
Example:
class SwitchFieldWidget extends form.StatefulFieldWidget<form.OptionSelect> {
const SwitchFieldWidget({required super.context});
@override
Widget buildWithTheme(BuildContext buildContext, FormTheme theme, form.FieldBuilderContext ctx) {
final field = ctx.field as form.OptionSelect;
final option = field.options.first;
return Switch(
value: ctx.isOptionSelected(option.value),
onChanged: (bool value) {
ctx.toggleValue(option);
},
);
}
}Implementation Detail: This method queries the current field value (which is a List<FieldOption> for multiselect fields) and checks if any option has a matching value property.
void toggleValue(FieldOption option)Description: Toggles the selection state of an option in a multiselect field.
Parameters:
option- TheFieldOptionto toggle
Behavior:
- If option is selected: Removes it from the selection
- If option is not selected: Adds it to the selection
- Triggers onChange callback
- Notifies controller listeners
- Validates field if
validateLiveis enabled
Use Case: Handling user interactions with checkboxes, switches, and chip selects.
Example:
class CheckboxFieldWidget extends form.StatefulFieldWidget<form.OptionSelect> {
const CheckboxFieldWidget({required super.context});
@override
Widget buildWithTheme(BuildContext buildContext, FormTheme theme, form.FieldBuilderContext ctx) {
final field = ctx.field as form.OptionSelect;
return Column(
children: field.options.map((option) {
return CheckboxListTile(
title: Text(option.label),
value: ctx.isOptionSelected(option.value),
onChanged: (bool? checked) {
ctx.toggleValue(option);
},
);
}).toList(),
);
}
}Alternative Pattern: For more control, you can manually manage the selected options list:
// Get current selections
final selectedOptions = ctx.getValue<List<form.FieldOption>>() ?? [];
// Add option
final updated = [...selectedOptions, newOption];
ctx.setValue(updated);
// Remove option
final updated = selectedOptions.where((o) => o.value != optionToRemove.value).toList();
ctx.setValue(updated);See Also: isOptionSelected, setValue
class EmailFieldWidget extends form.StatefulFieldWidget<form.Field> {
const EmailFieldWidget({required super.context});
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
return TextField(
controller: ctx.getTextController(),
focusNode: ctx.getFocusNode(),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: ctx.field.title,
hintText: 'user@example.com',
border: OutlineInputBorder(
borderSide: BorderSide(color: ctx.colors.borderColor),
),
),
);
}
}class RatingFieldWidget extends form.StatefulFieldWidget<form.Field> {
const RatingFieldWidget({required super.context});
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
final rating = ctx.getValue<int>() ?? 0;
return Row(
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < rating ? Icons.star : Icons.star_border,
color: ctx.colors.iconColor,
),
onPressed: () => ctx.setValue<int>(index + 1),
);
}),
);
}
}class PasswordConfirmField extends form.StatefulFieldWidget<form.Field> {
const PasswordConfirmField({required super.context});
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
// Access another field's value
final password = ctx.controller.getFieldValue<String>('password');
final confirm = ctx.getValue<String>() ?? '';
// Custom validation
if (confirm.isNotEmpty && password != confirm) {
ctx.clearErrors();
ctx.addError('Passwords do not match');
}
return TextField(
controller: ctx.getTextController(),
obscureText: true,
decoration: InputDecoration(
labelText: 'Confirm Password',
),
);
}
}class ConditionalFieldWidget extends form.StatefulFieldWidget<form.Field> {
const ConditionalFieldWidget({required super.context});
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: ctx.state == FieldState.error
? theme.errorColors.backgroundColor
: ctx.colors.backgroundColor,
border: Border.all(
color: ctx.colors.borderColor,
width: ctx.state == FieldState.active ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
TextField(controller: ctx.getTextController()),
if (ctx.state == FieldState.error)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'Please fix the errors above',
style: TextStyle(color: theme.errorColors.textColor),
),
),
],
),
);
}
}Good:
final value = ctx.getValue<String>();
ctx.setValue('new value');
ctx.clearErrors();Avoid:
final value = ctx.controller.getFieldValue<String>(ctx.field.id);
ctx.controller.updateFieldValue(ctx.field.id, 'new value');
ctx.controller.clearErrors(ctx.field.id);Reason: Convenience methods are cleaner, less verbose, and less error-prone.
Good:
// Only create when needed
if (needsTextEditing) {
final controller = ctx.getTextController();
// Use controller
}Avoid:
// Don't create unnecessarily
@override
void initState() {
super.initState();
final controller = ctx.getTextController(); // Created even if not used
}Reason: Lazy initialization improves performance by avoiding unnecessary resource allocation.
Good:
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: ctx.colors.borderColor),
),
),
)Avoid:
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(
color: ctx.state == FieldState.error
? ctx.theme.errorColors.borderColor
: ctx.state == FieldState.active
? ctx.theme.activeColors.borderColor
: ctx.theme.normalColors.borderColor,
),
),
),
)Reason: ctx.colors automatically selects the correct color scheme for the current state.
Good:
void validateEmail(String email) {
ctx.clearErrors(); // Clear previous errors first
if (email.isEmpty) {
ctx.addError('Email is required');
} else if (!isValidEmail(email)) {
ctx.addError('Invalid email format');
}
}Avoid:
void validateEmail(String email) {
// Errors accumulate without clearing
if (email.isEmpty) {
ctx.addError('Email is required');
}
if (!isValidEmail(email)) {
ctx.addError('Invalid email format');
}
}Reason: Prevents error accumulation and ensures only current errors are displayed.
Good:
@override
void onValueChanged(dynamic oldValue, dynamic newValue) {
if (context.field.onChange != null) {
final results = form.FormResults.getResults(controller: context.controller);
context.field.onChange!(results);
}
}Avoid:
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
return TextField(
onChanged: (value) {
ctx.setValue(value);
// Triggering onChange here causes it to fire multiple times per change
if (ctx.field.onChange != null) {
final results = form.FormResults.getResults(controller: ctx.controller);
ctx.field.onChange!(results);
}
},
);
}Reason: onValueChanged hook is called once per value change, preventing duplicate callback invocations.
Validate this field based on another field's value:
class PasswordConfirmField extends form.StatefulFieldWidget<form.Field> {
const PasswordConfirmField({required super.context});
@override
Widget buildWithTheme(BuildContext context, FormTheme theme, form.FieldBuilderContext ctx) {
return TextField(
controller: ctx.getTextController(),
obscureText: true,
onChanged: (value) {
_validatePasswordMatch(ctx, value);
},
);
}
void _validatePasswordMatch(form.FieldBuilderContext ctx, String confirmValue) {
final password = ctx.controller.getFieldValue<String>('password');
ctx.clearErrors();
if (confirmValue.isNotEmpty && password != confirmValue) {
ctx.addError('Passwords do not match');
}
}
}// Focus this field programmatically
ctx.controller.setFieldFocus(ctx.field.id, true);
// Focus another field
ctx.controller.setFieldFocus('other_field_id', true);
// Remove focus from all fields
ctx.controller.setFieldFocus(ctx.field.id, false);Update value without triggering onChange or rebuilding widgets:
// Silent update (useful for initialization)
ctx.setValue('initial value', noNotify: true);
// Normal update (triggers onChange and rebuilds)
ctx.setValue('user input');Get all currently rendered fields:
final activeFields = ctx.controller.activeFields;
print('Form has ${activeFields.length} active fields');
// Check if a specific field is active
final isActive = activeFields.any((f) => f.id == 'email');- StatefulFieldWidget Guide - Base class using FieldBuilderContext
- Custom Field Cookbook - Practical examples
- Converters Guide - Type conversion patterns
- Migration Guide v0.5.x → v0.6.0 - Upgrade instructions
FieldBuilderContext dramatically simplifies custom field development by:
✅ Bundling 6 parameters into 1 context object ✅ Providing convenient helper methods for common operations ✅ Supporting lazy resource initialization for performance ✅ Enabling theme-aware field development ✅ Exposing full controller access for advanced use cases
With FieldBuilderContext, custom field boilerplate is reduced from 120-150 lines to 30-50 lines (60-70% reduction).