A declarative Flutter form builder focusing on clean structure, easy validation, state management via a central controller, and customizable theming. Designed to simplify form creation and management.
📚 Complete Documentation - Comprehensive guides, API references, and examples
- Custom Field Cookbook - 6 practical examples for creating custom fields
- Migration Guide v0.5.x → v0.6.0 - Upgrade to simplified custom field API
- Migration Guide v0.3.x → v0.4.0 - Upgrade to namespace-based API
- Declarative Field Definition: Define form fields using clear widget classes (
form.TextField,form.OptionSelect,form.CheckboxSelect,form.FileUpload, etc.). - Layout Control: Structure forms visually using
form.Rowandform.Columnwidgets for flexible layouts (responsive collapsing included). - Centralized State Management: Uses
form.FormControllerto manage field values, focus, and validation state. Access and update form state from anywhere. - Built-in Validation: Add validators easily using
form.Validatorand leverage providedform.Validators(email, empty, length, numeric, files) or create custom ones. Live validation option available. - Result Handling: Simple API (
form.FormResults.getResults,results.grab(...)) to retrieve formatted form data (asString,asStringList,asMultiselectList,asFile,asCompound). - Theming: Customize the look and feel using
FormTheme. Apply themes globally (FormThemesingleton), per-form, or per-field. Includes pre-built themes. - Autocomplete: Add autocomplete suggestions to
form.TextFieldusingform.AutoCompleteBuilder, supporting initial lists and asynchronous fetching. - File Uploads:
form.FileUploadwidget integrates withfile_pickerand supports drag-and-drop, file type restrictions (allowedExtensions), and validation. - Controller Interaction: Programmatically update field values (
updateTextFieldValue,toggleMultiSelectValue) and clear selections (removeMultiSelectOptions). - 🆕 Compound Fields: Create reusable composite fields (like
form.NameField,form.AddressField) that group multiple sub-fields with custom layouts. Sub-fields act as independent fields with automatic ID prefixing. - 🆕 Simplified Custom Fields (v0.6.0+): Create custom fields with 60-70% less boilerplate using
StatefulFieldWidgetandFieldBuilderContext.
ChampionForms now supports compound fields - composite fields made up of multiple sub-fields that work together as a cohesive unit while maintaining full controller transparency.
Key Features:
- Built-in Fields:
form.NameFieldandform.AddressFieldready to use out of the box - Automatic ID Prefixing: Sub-fields get prefixed IDs (e.g.,
address_street,address_city) to prevent conflicts - Controller Transparency: Sub-fields behave like normal fields - all existing controller methods work unchanged
- Custom Layouts: Each compound field can have its own layout (horizontal, multi-row, etc.)
- Flexible Results Access: Access compound values as joined strings or individual sub-field values
Example Usage:
// Use built-in NameField
form.NameField(
id: 'customer_name',
title: 'Full Name',
includeMiddleName: true, // Optional middle name field
)
// Use built-in AddressField
form.AddressField(
id: 'shipping_address',
title: 'Shipping Address',
includeStreet2: true, // Optional apt/suite field
includeCountry: false, // Optional country field
)
// Access results
final fullName = results.grab('customer_name').asCompound(delimiter: ' ');
final street = results.grab('shipping_address_street').asString();
final fullAddress = results.grab('shipping_address').asCompound(delimiter: ', ');Create Custom Compound Fields: Register your own reusable compound fields with custom sub-field definitions and layouts:
FormFieldRegistry.registerCompound<MyCustomField>(
'custom',
(field) => [
form.TextField(id: 'sub1'),
form.TextField(id: 'sub2'),
],
(context, subFields, errors) => Row(
children: subFields.map((f) => Expanded(child: f)).toList(),
),
);See the example app's compound fields demo for a complete interactive demonstration.
Version 0.6.0 dramatically simplifies custom field creation by reducing boilerplate from 120-150 lines to 30-50 lines (60-70% reduction).
New Features:
- FieldBuilderContext - Bundles 6 parameters into one clean context object
- StatefulFieldWidget - Abstract base class with automatic lifecycle management
- Converter Mixins - Reusable type conversion logic
- Simplified FormFieldRegistry - Static registration methods
Who This Affects:
- âś… Custom field developers: If you've created custom field types, you need to migrate
- ❌ Regular users: If you only use built-in fields (TextField, OptionSelect, FileUpload), no changes required
Migration:
- Migration Guide v0.5.x → v0.6.0 - Complete upgrade instructions
- Custom Field Cookbook - Updated examples using new API
- StatefulFieldWidget Guide - Learn the base class pattern
- FieldBuilderContext API - Context object reference
Version 0.4.0 modernizes the ChampionForms API by removing the "Champion" prefix from all classes and adopting idiomatic Dart namespace patterns.
Key Changes:
- Cleaner class names:
ChampionTextField→form.TextField - Namespace import approach to avoid collisions with Flutter widgets
- Two-tier export system (form lifecycle vs. theming/configuration)
- No functional changes - all behavior remains identical
Migration Required: If you're upgrading from v0.3.x, please see the Migration Guide v0.3.x → v0.4.0.
- Layout Widgets: Introduced
form.Rowandform.Columnfor structuring form layouts. Columns can collapse vertically using thecollapseflag. - File Upload Field: Added
form.FileUploadwith drag-and-drop,file_pickerintegration,allowedExtensionsfor filtering, and newform.Validatorsmethods (fileIsImage,fileIsDocument,isMimeType).- Important: Using
file_picker(and thusform.FileUpload) requires adding platform-specific permissions (iOSInfo.plist, AndroidAndroidManifest.xml, macOS*.entitlements). Please refer to thefile_pickerdocumentation for details.
- Important: Using
- Autocomplete: Implemented
form.AutoCompleteBuilderforform.TextFieldto provide input suggestions. Supports initial options, asynchronous fetching (e.g., from APIs), debouncing, and basic customization. - Global Theming: Added
FormThemesingleton to set a defaultFormThemefor allform.Forminstances in your app. - Enhanced Controller:
form.FormControllernow managesTextEditingControllerlifecycles internally, tracks actively rendered fields (activeFields), supports field grouping by page (pageName,getPageFields), and includes new helper methods likeupdateTextFieldValue,toggleMultiSelectValue, andremoveMultiSelectOptions. - New Validators: Added file-specific validators to
form.Validators. - Various Fixes: Addressed issues with
asStringList, multiselect default values, controller updates, and more.
Add the following to your pubspec.yaml:
dependencies:
championforms: ^0.6.0 # Use the latest version
flutter:
sdk: flutter
# Required for File Uploads:
file_picker: ^10.0.0 # Or latest compatible version
desktop_drop: ^0.7.0 # Or latest compatible version (for drag-and-drop on desktop)
mime: ^2.0.0 # Or latest compatible version
# Other dependencies if needed (e.g., email_validator for default email check)
email_validator: ^3.0.0Then run flutter pub get.
Important: ChampionForms v0.4.0+ uses a namespace import approach. Import the library as:
import 'package:championforms/championforms.dart' as form;This prevents naming collisions with Flutter's built-in Form, Row, and Column widgets.
Remember to configure platform permissions for file_picker if using form.FileUpload.
import 'package:flutter/material.dart';
import 'package:championforms/championforms.dart' as form;
// For theming (typically once in main.dart):
// import 'package:championforms/championforms_themes.dart';
// 1. Define your Controller
final formController = form.FormController();
// Optional: Set a Global Theme (e.g., in main() or a root widget)
// Needs context, so often done within a build method or builder initially.
// FormTheme.instance.setTheme(softBlueColorTheme(context));
// 2. Define your Fields
final List<form.FieldBase> myFields = [
form.Row(columns: [
form.Column(fields: [
form.TextField(
id: "name",
textFieldTitle: "Name",
validators: [
form.Validator(
validator: (r) => form.Validators.isEmpty(r),
reason: "Name is required"
)
],
),
]),
form.Column(fields: [
form.TextField(
id: "email",
textFieldTitle: "Email",
autoComplete: form.AutoCompleteBuilder(
initialOptions: [form.CompleteOption(value:"suggestion@example.com")]
),
validators: [
form.Validator(
validator: (r) => form.Validators.isEmail(r),
reason: "Invalid Email"
)
]
),
]),
]),
form.FileUpload(
id: "avatar",
title: "Upload Avatar (PNG only)",
allowedExtensions: ['png'],
validators: [
form.Validator(
validator: (r) => form.Validators.fileIsImage(r),
reason:"Must be an image"
)
]
),
// ... other fields (Dropdown, Checkbox, etc.)
];
// 3. Build the Form Widget
Widget build(BuildContext context) {
return Scaffold(
body: form.Form(
controller: formController,
fields: myFields,
spacing: 10.0, // Optional spacing between fields
// theme: myCustomTheme, // Optional: Override global theme
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 4. Get Results & Validate
final form.FormResults results = form.FormResults.getResults(controller: formController);
if (!results.errorState) {
print("Name: ${results.grab("name").asString()}");
print("Email: ${results.grab("email").asString()}");
final files = results.grab("avatar").asFile();
if(files.isNotEmpty) print("Avatar filename: ${files.first.name}");
} else {
print("Form has errors: ${results.formErrors}");
}
},
child: Icon(Icons.check),
),
);
}Creating custom fields is now dramatically simpler with the v0.6.0 API:
import 'package:flutter/material.dart';
import 'package:championforms/championforms.dart' as form;
class RatingFieldWidget extends form.StatefulFieldWidget {
const RatingFieldWidget({required super.context});
@override
Widget buildWithTheme(
BuildContext buildContext,
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),
);
}),
);
}
@override
void onValueChanged(dynamic oldValue, dynamic newValue) {
if (context.field.onChange != null) {
final results = form.FormResults.getResults(controller: context.controller);
context.field.onChange!(results);
}
}
}That's it! ~30 lines vs ~120 lines in the old API.
Note: If you create a custom field class that extends
form.Field(not justStatefulFieldWidget), you must implement thecopyWithmethod. This is required for proper field copying in compound fields and state propagation. See the Custom Field Cookbook for details.
For more examples, see the Custom Field Cookbook with 6 complete, working examples:
- Phone number field with formatting
- Tag selector with autocomplete
- Rich text editor field
- Date/time picker field
- Signature pad field
- File upload with preview enhancement
The controller is the heart of state management.
- Initialization:
final controller = form.FormController(); - Disposal: Crucial! Call
controller.dispose();in yourStatefulWidget'sdisposemethod. - Getting Results: Use
form.FormResults.getResults(controller: controller)to trigger validation and get current values. - Updating Values Programmatically:
controller.updateTextFieldValue("fieldId", "new text");controller.toggleMultiSelectValue("checkboxFieldId", toggleOn: ["value1", "value2"], toggleOff: ["value3"]);(Uses thevalueofform.FieldOption)
- Clearing Selections:
controller.removeMultiSelectOptions("fileUploadId");(Clears all selected files/options)
- Accessing State:
controller.findTextFieldValue("fieldId")?.value;controller.findMultiselectValue("fieldId")?.values;(ReturnsList<form.FieldOption>)controller.isFieldFocused("fieldId");controller.findErrors("fieldId");
- Page Management:
- Fields can be assigned to a page using the
pageNameproperty onform.Form. controller.getPageFields("pageName");retrieves theform.Fieldlist for that page. Useful for partial validation or results.
- Fields can be assigned to a page using the
- Active Fields:
controller.activeFieldscontains the list ofform.Fieldcurrently rendered by linkedform.Formwidgets.
Standard text input.
id: Unique identifier.textFieldTitle: Label text that animates to the border.hintText: Placeholder text inside the field.description: Text displayed above the field.maxLines: Number of lines (1 for single line,nullor >1 for multiline).password: Obscures text if true.leading/trailing/icon: Widgets for icons/buttons around the field.validateLive: Validate field on focus loss.validators: List ofform.Validator.defaultValue: Initial text value.onSubmit: Callback triggered on Enter key press (ifmaxLinesis 1 ornull).onChange: Callback triggered on every character change.autoComplete: Instance ofform.AutoCompleteBuilderto enable suggestions.
Base class for fields with multiple options.
id: Unique identifier.title/description: Field labels.options:List<form.FieldOption>defining the choices.form.FieldOption(label: "Display Text", value: "submitted_value", additionalData: optionalObject)
multiselect: Allow multiple selections if true.defaultValue:List<String>of values to select by default.validators,validateLive,onSubmit,onChange: Standard properties.fieldBuilder: Function to build the actual UI (defaults to dropdown).
Convenience widget using form.OptionSelect with a checkbox builder.
- Inherits properties from
form.OptionSelect. - Renders options as a list of checkboxes.
Specialized field for file uploads.
- Inherits properties from
form.OptionSelect(options list is unused internally). multiselect: Allow multiple file uploads.allowedExtensions:List<String>(e.g.,['pdf', 'docx']) to filter files in the picker and during drag-and-drop.displayUploadedFiles: Show previews/icons of uploaded files (default: true).dropDisplayWidget: Customize the appearance of the drag-and-drop zone.validators: Useform.Validators.isEmpty,form.Validators.fileIsImage(results),form.Validators.fileIsDocument(results), etc.- Permissions: Requires platform setup for
file_picker.
Layout widgets.
form.Row: Arrangesform.Columnwidgets horizontally.columns:List<form.Column>.collapse: If true, stacks columns vertically.rollUpErrors: If true, displays errors from all child fields below the row.
form.Column: Arranges standard fields vertically.fields:List<form.FieldBase>(can includeform.TextField,form.Row, etc.).columnFlex:intvalue forFlexiblewidget controlling width distribution within aform.Row.rollUpErrors: If true, displays errors from all child fields below the column.
- Assign a
List<form.Validator>to thevalidatorsproperty of a field. form.Validator(validator: (results) => /* boolean logic */, reason: "Error message")resultsis aform.FieldResultsobject containing the current field value(s). Access data usingresults.asString(),results.asMultiselectList(),results.asFile(), etc.- Use
form.Validatorsfor common checks:isEmpty(results)isEmail(results)isInteger(results),isDouble(results)(andOrNullvariants)isMimeType(results, ['image/jpeg', 'image/png'])fileIsImage(results)fileIsCommonImage(results)fileIsDocument(results)
- Set
validateLive: trueto trigger validation when a field loses focus. - Validation is always run when
form.FormResults.getResults()is called.
ChampionForms uses a FormTheme object to control appearance.
- Hierarchy: Default Theme -> Global Theme (Singleton) ->
form.FormTheme -> Field Theme. Specific settings override general ones. FormThemeProperties: DefineFieldColorSchemefor different states (normal, error, active, disabled, selected),TextStyles (title, description, hint, chip), andInputDecoration.FieldColorScheme: Defines colors (background, border, text, icon, hint) and gradients.- Setting Global Theme:
// Import the themes export file import 'package:championforms/championforms_themes.dart'; // Somewhere early in your app (needs context) FormTheme.instance.setTheme(softBlueColorTheme(context));
- Setting Form Theme:
// Import the themes export file import 'package:championforms/championforms_themes.dart'; form.Form( theme: redAccentFormTheme(context), // Pass a theme object controller: controller, fields: fields, )
- Pre-built Themes:
softBlueColorTheme,redAccentFormTheme,iconicColorThemeare provided. Create your own by defining aFormThemeobject.
Note: Theme-related classes (FormTheme, pre-built themes, FormFieldRegistry) are exported from package:championforms/championforms_themes.dart and don't use the form. namespace prefix.
- Call
form.FormResults results = form.FormResults.getResults(controller: controller); - Check
results.errorState(boolean). - If no errors, access field data:
results.grab("fieldId")returnsform.FieldResults. - Format the
form.FieldResults:.asString(): Returns the value(s) as a single string..asStringList(): Returns values asList<String>..asBool()/.asBoolMap(): Interprets values as booleans..asMultiselectList(): Returns selected options asList<form.FieldOption>..asMultiselectSingle(): Returns the first selected option asform.FieldOption?..asFile(): Returns uploaded files asList<form.FileResultData>, containing name, path, andFileModel(with bytes/stream/MIME details).
Contributions, issues, and feature requests are welcome!
MIT License.