form_flutter
A Flutter package for building reusable, schema-friendly forms with shared validation, common field widgets, and preset field catalogs.
Features
- Reusable form controller for values, errors, async validation state, serialization, reset flows, and touched or dirty tracking
- Common field widgets for text, password, multiline, number, phone, country, dropdown, radio, checkbox, switch, date, time, date-time, slider, and multi-select
- Sync and async validators for text, numbers, dates, files, conditional rules, and uniqueness checks
- Preset field catalog for personal, contact, address, account, academic, professional, survey, commerce, appointment, and consent flows
- Schema builder helpers for turning presets and schema definitions into full sections, controllers, and generated fields with per-field overrides
- Common option sets for fields like gender, marital status, degree, employment type, payment method, and more
- Dedicated example app and local playground demonstrating controller export, import, reset, live state previews, and schema-generated forms
What's New In 1.0.3
- Added
FormFlutterSchemaFormfor rendering complete schema-driven forms with one widget - Added production-ready schema templates for registration, profile, job application, appointment, feedback, and checkout flows
- Added richer form controls including
onChanged, reset button support, button styling hooks, and catalog lookup helpers - Fixed async validation disposal safety and autocomplete stale-selection handling
Getting started
Add the package to your pubspec.yaml:
dependencies:
form_flutter: ^1.0.3
Then run:
flutter pub get
Usage
Import the package:
import 'package:form_flutter/form_flutter.dart';
Create a controller and define your fields:
final controller = FormFlutterController(
initialValues: const {
'fullName': '',
'email': '',
'phone': '',
'phoneCountry': 'US',
'team': 'product',
'acceptTerms': false,
},
);
final fields = <FormFlutterField<dynamic>>[
FormFlutterTextField(
name: 'fullName',
label: 'Full name',
validator: FormFlutterValidators.combine([
FormFlutterValidators.requiredText(),
FormFlutterValidators.minLength(3),
]),
),
FormFlutterTextField(
name: 'email',
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: FormFlutterValidators.combine([
FormFlutterValidators.requiredText(),
FormFlutterValidators.email(),
]),
asyncValidator: FormFlutterValidators.uniqueEmail(
(email, _) async => email != 'taken@example.com',
),
),
FormFlutterPhoneField(
name: 'phone',
label: 'Phone',
countryFieldName: 'phoneCountry',
validator: FormFlutterValidators.combine([
FormFlutterValidators.requiredText(),
FormFlutterValidators.numericText(),
]),
),
FormFlutterDropdownField<String>(
name: 'team',
label: 'Team',
options: const [
FormFlutterOption(value: 'design', label: 'Design'),
FormFlutterOption(value: 'product', label: 'Product'),
FormFlutterOption(value: 'engineering', label: 'Engineering'),
],
validator: FormFlutterValidators.requiredSelection<String>(),
),
FormFlutterCheckboxField(
name: 'acceptTerms',
label: 'I agree to the Terms and Conditions',
validator: FormFlutterValidators.mustBeTrue(),
),
];
Render them with DynamicFormFlutter:
DynamicFormFlutter(
controller: controller,
fields: fields,
submitLabel: 'Submit',
onSubmit: (values) {
debugPrint('Submitted: ${values.asMap()}');
},
)
Quick Example
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final controller = FormFlutterController(
initialValues: const {
'fullName': '',
'email': '',
'phone': '',
'phoneCountry': 'US',
'newsletter': true,
},
);
late final fields = <FormFlutterField<dynamic>>[
FormFlutterTextField(
name: 'fullName',
label: 'Full name',
validator: FormFlutterValidators.combine([
FormFlutterValidators.requiredText(),
FormFlutterValidators.minLength(3),
]),
),
FormFlutterTextField(
name: 'email',
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: FormFlutterPresetValidators.emailAddress(),
),
FormFlutterPhoneField(
name: 'phone',
label: 'Phone',
countryFieldName: 'phoneCountry',
validator: FormFlutterValidators.combine([
FormFlutterValidators.requiredText(),
FormFlutterValidators.numericText(),
]),
),
FormFlutterSwitchField(
name: 'newsletter',
label: 'Receive product updates',
),
];
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DynamicFormFlutter(
controller: controller,
fields: fields,
submitLabel: 'Create account',
onSubmit: (values) {
final json = controller.toJson();
debugPrint('Submitted values: ${values.asMap()}');
debugPrint('Serialized snapshot: $json');
},
);
}
}
For complete runnable samples, see example/lib/main.dart and lib/main.dart.
Schema builder
You can also generate a full form from schema definitions and preset metadata.
final schema = FormFlutterSchema(
initialValues: const {
'phoneCountry': 'US',
'newsletter': true,
},
sections: [
FormFlutterSchemaSection(
title: 'Account',
description: 'Create the basics once and override only what this screen needs.',
fields: [
FormFlutterSchemaField.fromPreset(
FormFlutterCatalog.accountFields.firstWhere(
(preset) => preset.key == 'username',
),
label: 'Public username',
helperText: 'Shown on your profile.',
hintText: '@ada',
initialValue: 'ada',
decorationOverride: const InputDecoration(prefixIcon: Icon(Icons.alternate_email)),
),
FormFlutterSchemaField.fromPreset(
FormFlutterCatalog.contactInformation.firstWhere(
(preset) => preset.key == 'phone',
),
countryFieldName: 'phoneCountry',
allowedCountryCodes: const ['US', 'CA'],
nationalNumberHintText: '5551234567',
),
],
),
const FormFlutterSchemaSection(
title: 'Preferences',
fields: [
FormFlutterSchemaField(
name: 'experience',
kind: FormFlutterFieldKind.slider,
label: 'Experience',
sliderMin: 1,
sliderMax: 10,
sliderDivisions: 9,
sliderUnitLabel: 'years',
),
],
),
],
);
final controller = FormFlutterFieldFactory.buildControllerFromSchema(schema);
final sections = FormFlutterFieldFactory.buildSectionsFromSchema(schema);
DynamicFormFlutter(
controller: controller,
fields: const [],
sections: sections,
onSubmit: (values) {
debugPrint('Schema submit: ${values.asMap()}');
},
)
For the shortest production-style setup, use a built-in schema template with
FormFlutterSchemaForm:
FormFlutterSchemaForm(
schema: FormFlutterSchemaTemplates.jobApplication(
initialValues: const {'industry': 'technology'},
),
useStepper: true,
showResetButton: true,
showValidationSummary: true,
scrollToFirstError: true,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (values) {
debugPrint('Draft changed: ${values.asMap()}');
},
onSubmit: (values) {
debugPrint('Job application: ${values.asMap()}');
},
)
Built-in schema templates include:
FormFlutterSchemaTemplates.accountRegistration()FormFlutterSchemaTemplates.profile()FormFlutterSchemaTemplates.jobApplication()FormFlutterSchemaTemplates.appointmentBooking()FormFlutterSchemaTemplates.feedback()FormFlutterSchemaTemplates.checkout()
Schema helpers include:
FormFlutterSchema,FormFlutterSchemaSection, andFormFlutterSchemaFieldFormFlutterSchemaFormfor rendering a complete schema with one widgetFormFlutterSchemaTemplatesfor common professional workflowsFormFlutterSchemaField.fromPreset(...)for merging preset defaults with screen-specific overridesFormFlutterFieldFactory.buildFieldsFromSchema(...)for flat formsFormFlutterFieldFactory.buildSectionsFromSchema(...)for sectioned or stepper formsFormFlutterFieldFactory.buildInitialValuesFromSchema(...)andbuildControllerFromSchema(...)for form state setup- typed overrides like
label,helperText,hintText,options,validator,asyncValidator, phone-country config, slider ranges,decorationOverride, andtextStyle
Controller helpers
FormFlutterController now includes state and serialization helpers for real-world form flows.
final snapshot = controller.toJson();
controller.fromJson(snapshot);
controller.reset();
controller.resetField('email');
final isEmailTouched = controller.isTouched('email');
final isEmailDirty = controller.isDirty('email');
final hasUnsavedChanges = controller.hasDirtyFields;
Available controller helpers include:
toJsonandfromJsonfor serializing and restoring valuesresetandresetFieldfor full-form and per-field reset flowsisTouched,isDirty,hasTouchedFields, andhasDirtyFieldsfor change trackingtouchedFieldsListenableanddirtyFieldsListenablefor reactive UI updatesvaluesListenable,errorsListenable, andasyncStatesListenablefor controller-driven widgets
Serialization notes:
DateTimevalues are exported with a tagged JSON object and restored automaticallyTimeOfDayvalues are exported with a tagged JSON object and restored automaticallyFormFlutterFileValuevalues are exported with name, size, extension, and MIME metadata
API overview
| Area | Main APIs |
|---|---|
| Controller | FormFlutterController, FormFlutterValues |
| Form widget | DynamicFormFlutter |
| Text-like fields | FormFlutterTextField, FormFlutterNumberField, FormFlutterPhoneField, FormFlutterCountryField |
| Choice fields | FormFlutterDropdownField, FormFlutterRadioGroupField, FormFlutterMultiSelectField |
| Toggle fields | FormFlutterCheckboxField, FormFlutterSwitchField |
| Date and time fields | FormFlutterDateField, FormFlutterTimeField, FormFlutterDateTimeField |
| Range fields | FormFlutterSliderField |
| Validators | FormFlutterValidators, FormFlutterPresetValidators |
| Catalog and presets | FormFlutterCatalog, FormFlutterOptionSets, FormFlutterFieldPreset |
| Schema builder | FormFlutterSchema, FormFlutterSchemaSection, FormFlutterSchemaField, FormFlutterSchemaForm, FormFlutterSchemaTemplates, FormFlutterFieldFactory |
| Supporting models | FormFlutterOption, FormFlutterFileValue |
Typical flow:
- Create a
FormFlutterControllerwithinitialValues. - Define a list of
FormFlutterField<dynamic>widgets. - Attach validators and async validators where needed.
- Render everything with
DynamicFormFlutter. - Read
values.asMap()on submit or persist withcontroller.toJson().
Styling and customization
You can customize field visuals at both the field level and the option level.
Field customization example:
FormFlutterTextField(
name: 'fullName',
label: 'Full name',
decorationOverride: InputDecoration(
prefixIcon: const Icon(Icons.person_outline),
fillColor: const Color(0xFFF8FBFF),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
),
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
),
)
Option customization example:
FormFlutterDropdownField<String>(
name: 'team',
label: 'Team',
options: const [
FormFlutterOption(
value: 'design',
label: 'Design',
color: Color(0xFF7C3AED),
icon: Icons.brush_outlined,
indicatorSize: 16,
),
FormFlutterOption(
value: 'engineering',
label: 'Engineering',
color: Color(0xFF2563EB),
icon: Icons.code,
indicatorSize: 16,
),
],
)
Multi-select fields also support a full custom option builder:
FormFlutterMultiSelectField<String>(
name: 'interests',
label: 'Interests',
options: const [
FormFlutterOption(
value: 'analytics',
label: 'Analytics',
color: Color(0xFFDC2626),
backgroundColor: Color(0xFFFEF2F2),
selectedColor: Color(0xFFDC2626),
selectedTextColor: Colors.white,
icon: Icons.monitoring_outlined,
),
],
optionBuilder: (context, option, isSelected) {
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? (option.selectedColor ?? option.color ?? Colors.blue)
: (option.backgroundColor ?? Colors.white),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: option.borderColor ?? option.color ?? const Color(0xFFD0D5DD),
),
),
child: Text(option.label),
);
},
)
Available visual controls include:
onChanged,onReset,showResetButton,resetLabel,submitButtonStyle, andsecondaryButtonStyleonDynamicFormFlutterandFormFlutterSchemaFormdecorationOverrideandtextStylefor text/date/time-like fieldsdecoration,titleStyle,activeColor,checkColor,thumbColor, andtrackColorfor toggle-style fieldsactiveColor,inactiveColor,thumbColor, andvalueStylefor sliderscolor,backgroundColor,selectedColor,textColor,selectedTextColor,borderColor,indicatorSize, andiconfor optionsoptionBuilderfor custom dropdown, radio, and multi-select option rendering
Validators
Available validator helpers include:
requiredText,requiredNumber,requiredSelection,requiredDate,requiredTime,requiredValueemail,phone,url,numericText,patternminLength,maxLength,exactLengthminNumber,maxNumberminDate,maxDate,minimumAgesameAsField,requiredIf,conditionalminItems,maxItemsrequiredFile,fileSize,fileExtension,imageOnlystrongPassword,mediumPassword,highStrengthPassworduniqueValue,uniqueUsername,uniqueEmailcombine,combineAsync,custom
Preset validator helpers also include:
FormFlutterPresetValidators.emailAddress()FormFlutterPresetValidators.phoneNumber()FormFlutterPresetValidators.password()FormFlutterPresetValidators.strongPassword()FormFlutterPresetValidators.highStrengthPassword()FormFlutterPresetValidators.confirmPassword()FormFlutterPresetValidators.otp()FormFlutterPresetValidators.uniqueUsername()FormFlutterPresetValidators.uniqueEmail()
Preset catalog
form_flutter includes metadata presets and option sets to help build real-world forms faster.
Examples:
FormFlutterCatalog.personalInformationFormFlutterCatalog.contactInformationFormFlutterCatalog.addressInformationFormFlutterCatalog.accountFieldsFormFlutterCatalog.professionalFieldsFormFlutterCatalog.appointmentFieldsFormFlutterCatalog.consentFields
Examples of shared option sets:
FormFlutterOptionSets.genderFormFlutterOptionSets.maritalStatusFormFlutterOptionSets.degreesFormFlutterOptionSets.employmentTypesFormFlutterOptionSets.paymentMethods
Additional information
This package includes:
- a dedicated
example/app for pub.dev example points - optional demo tooling (
device_preview) scoped toexample/only - a root app in
lib/main.dartthat acts as a local full-form playground - live examples of controller export, import, reset, touched or dirty state previews, and schema-generated forms
- controller tests covering serialization, reset behavior, and field-state tracking
If you want to explore the most complete demos, start with example/lib/main.dart and lib/main.dart.