flutter_form_builder_pro 1.0.1
flutter_form_builder_pro: ^1.0.1 copied to clipboard
A schema-driven Flutter form engine with conditional fields, async validation, multi-step wizard support, and JSON/YAML schema loading.
flutter_form_builder_pro
Schema-driven Flutter form engine
Define a schema → get a complete, validated, reactive form.
No boilerplate. No repeated validation logic. No hand-wired state.
Why flutter_form_builder_pro? #
Every Flutter app has forms — login, registration, checkout, onboarding, settings. Most solutions make you:
- Hand-wire every
TextEditingControllerandFocusNode - Re-implement validation logic on every screen
- Rebuild multi-step wizard state from scratch each time
- Write 80 lines of
Column+ifstatements for conditional fields
flutter_form_builder_pro fixes all of this. You write a schema describing what your form contains. The engine figures out how to build it, validate it, and hand you typed output.
// Before flutter_form_builder_pro — ~80 lines of boilerplate per form
// After flutter_form_builder_pro — this is the entire form:
SmartForm(
schema: FormSchema(
fields: [
FieldSchema.text(id: 'name', label: 'Full name',
validators: [Validators.required()]),
FieldSchema.email(id: 'email', label: 'Email',
validators: [Validators.required(), Validators.email()]),
FieldSchema.dropdown(id: 'country', label: 'Country',
options: OptionsSource.static([...]),
validators: [Validators.required()]),
// Appears only when country == India — zero extra code
FieldSchema.text(id: 'state', label: 'State',
visibleWhen: FieldCondition.equals('country', 'India')),
],
),
onSubmit: (values) async => await api.register(values),
)
Features #
| Feature | Details |
|---|---|
| Schema-driven | Define forms in Dart or load from JSON at runtime |
| Conditional fields | Show, hide, require, or disable fields based on other field values |
| Async validation | Server-side checks (email uniqueness, username availability) with Validators.remote() |
| Multi-step wizard | Built-in step controller, progress indicator, per-step validation, and step skipping |
| 20+ field types | text, email, password, phone, number, textarea, dropdown, radio, checkbox, checkbox group, date, slider, heading, divider, and more |
| Custom field types | Register any widget via FieldRegistry |
| JSON schema loading | FormSchema.fromJson() — load schemas from an API or remote config |
| Programmatic control | SmartFormController — pre-fill, reset, and submit a form from outside its widget tree |
| Autosave / drafts | Opt-in draft persistence via AutosaveConfig, with debounce and restore-on-mount |
| Fully themeable | Override decoration, labels, buttons, or the entire step indicator |
| Reactive state | ChangeNotifier-based — no external state management required |
| Dart 3 + null-safe | Sealed classes, exhaustive switches, strict analysis options |
Installation #
# pubspec.yaml
dependencies:
flutter_form_builder_pro: ^1.0.0
flutter pub get
Quick start #
1. Single-page form #
import 'package:flutter_form_builder_pro/flutter_form_builder_pro.dart';
SmartForm(
schema: FormSchema(
title: 'Create account',
fields: [
FieldSchema.text(
id: 'full_name',
label: 'Full name',
validators: [Validators.required(), Validators.minLength(2)],
),
FieldSchema.email(
id: 'email',
label: 'Email address',
validators: [Validators.required(), Validators.email()],
),
FieldSchema.password(
id: 'password',
label: 'Password',
validators: [
Validators.required(),
Validators.minLength(8),
],
),
],
),
onSubmit: (Map<String, dynamic> values) async {
await api.register(values);
},
)
2. Multi-step wizard #
SmartForm(
schema: FormSchema.wizard(
title: 'Onboarding',
steps: [
FormStep(
id: 'personal',
title: 'Personal info',
fields: [
FieldSchema.text(id: 'name', label: 'Full name',
validators: [Validators.required()]),
FieldSchema.email(id: 'email', label: 'Email',
validators: [Validators.required(), Validators.email()]),
],
),
FormStep(
id: 'business',
title: 'Business details',
// This entire step is skipped for individual accounts
visibleWhen: FieldCondition.equals('account_type', 'business'),
fields: [
FieldSchema.text(id: 'company', label: 'Company name',
validators: [Validators.required()]),
],
),
FormStep(
id: 'finish',
title: 'All done',
fields: [
FieldSchema.textarea(id: 'bio', label: 'Short bio'),
],
),
],
),
onSubmit: (values) async => await api.onboard(values),
onStepChange: (from, to) => analytics.track('wizard_step_$to'),
)
3. JSON-driven form (remote config / no-code) #
// Load schema from your API — the form builds itself
final response = await http.get(Uri.parse('/api/forms/registration'));
SmartForm(
schema: FormSchema.fromJson(jsonDecode(response.body)),
onSubmit: (values) => api.submit('registration', values),
)
Conditional fields #
Fields can be conditionally visible, required, or disabled based on other field values. Conditions are evaluated reactively — the form updates instantly as the user types.
// Simple equals
FieldSchema.text(
id: 'state',
label: 'State',
visibleWhen: FieldCondition.equals('country', 'India'),
)
// OR — show business name for business or freelancer accounts
FieldSchema.text(
id: 'company',
label: 'Company name',
visibleWhen: FieldCondition.or([
FieldCondition.equals('type', 'business'),
FieldCondition.equals('type', 'freelancer'),
]),
)
// AND — compound condition
FieldSchema.text(
id: 'gst_number',
label: 'GST number',
visibleWhen: FieldCondition.and([
FieldCondition.equals('country', 'India'),
FieldCondition.equals('type', 'business'),
]),
)
// Conditional required — field is always shown, but only required sometimes
FieldSchema.text(
id: 'referral_code',
label: 'Referral code',
requiredWhen: FieldCondition.equals('has_referral', true),
)
// Conditional disabled
FieldSchema.text(
id: 'email',
label: 'Email',
disabledWhen: FieldCondition.equals('sso_enabled', true),
)
Available condition operators #
| Operator | Usage |
|---|---|
FieldCondition.equals(id, value) |
field == value |
FieldCondition.notEquals(id, value) |
field != value |
FieldCondition.contains(id, value) |
List contains / String includes |
FieldCondition.notContains(id, value) |
Inverse of contains |
FieldCondition.greaterThan(id, num) |
Numeric > |
FieldCondition.lessThan(id, num) |
Numeric < |
FieldCondition.isEmpty(id) |
null, '', [] |
FieldCondition.isNotEmpty(id) |
Inverse of isEmpty |
FieldCondition.and(conditions) |
All must pass |
FieldCondition.or(conditions) |
At least one must pass |
FieldCondition.not(condition) |
Negates any condition |
Validation #
Built-in validators #
validators: [
Validators.required(), // Non-null, non-empty
Validators.minLength(8), // String length >= 8
Validators.maxLength(120), // String length <= 120
Validators.email(), // Valid email format
Validators.phone(), // Valid phone format
Validators.pattern(RegExp(r'^\d{6}$'), // RegExp match
message: 'Must be a 6-digit PIN'),
Validators.minValue(0), // Numeric >= 0
Validators.maxValue(100), // Numeric <= 100
Validators.matchField('password', // Must equal another field
message: 'Passwords do not match'),
]
Async / server-side validation #
Validators.remote((value, formValues) async {
final taken = await api.checkEmailTaken(value as String);
return taken ? 'This email is already registered' : null;
})
Async validators run in parallel with all other validators via Future.wait.
The form blocks submission until all async validators resolve.
Custom inline validator #
Validators.custom((value, formValues) {
if (value == formValues['username']) {
return 'Password cannot be the same as your username';
}
return null; // null = valid
})
Field types #
Text variants #
FieldSchema.text(id: 'name', label: 'Full name')
FieldSchema.textarea(id: 'bio', label: 'Bio', maxLines: 5)
FieldSchema.email(id: 'email', label: 'Email')
FieldSchema.password(id: 'password', label: 'Password') // toggleable visibility
FieldSchema.phone(id: 'phone', label: 'Phone')
FieldSchema.number(id: 'age', label: 'Age', min: 0, max: 120)
Selection #
FieldSchema.dropdown(
id: 'country',
label: 'Country',
options: OptionsSource.static([
FieldOption(value: 'IN', label: 'India'),
FieldOption(value: 'US', label: 'United States'),
]),
)
FieldSchema.radio(
id: 'plan',
label: 'Plan',
options: OptionsSource.static([
FieldOption(value: 'free', label: 'Free',
description: 'Up to 3 projects'),
FieldOption(value: 'pro', label: 'Pro',
description: 'Unlimited projects'),
]),
)
FieldSchema.checkbox(id: 'agree', label: 'I agree to the Terms')
FieldSchema.checkboxGroup(
id: 'interests',
label: 'Interests',
options: OptionsSource.static([...]),
maxSelections: 3,
)
Async options (loaded from an API) #
FieldSchema.dropdown(
id: 'city',
label: 'City',
options: OptionsSource.async(() async {
final cities = await api.getCities();
return cities.map((c) => FieldOption(value: c.id, label: c.name)).toList();
}),
)
// Shows a loading indicator while fetching, retry button on error.
// Result is cached — loader only runs once.
Date & range #
FieldSchema.date(
id: 'dob',
label: 'Date of birth',
max: DateTime.now(),
min: DateTime(1900),
)
FieldSchema.slider(
id: 'budget',
label: 'Monthly budget (₹)',
min: 0,
max: 100000,
step: 1000,
initialValue: 10000,
)
Layout / decorative #
FieldSchema.heading(id: 'h1', label: 'Account details',
helperText: 'These details appear on your invoices.')
FieldSchema.divider(id: 'div1')
Controlling a form programmatically #
Use SmartFormController whenever you need to read or change a form's values
from outside its own widget — pre-filling after an async API call, resetting
on a button tap, or triggering submission from a different part of the screen.
class MyFormPage extends StatefulWidget {
const MyFormPage({super.key});
@override
State<MyFormPage> createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _controller = SmartFormController();
@override
void initState() {
super.initState();
_prefillFromApi();
}
Future<void> _prefillFromApi() async {
final profile = await api.getProfile();
// Safe to call even before SmartForm has mounted — values are queued
// and applied automatically once the form attaches.
_controller.setValues({
'name': profile.name,
'email': profile.email,
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SmartForm(
controller: _controller,
schema: mySchema,
onSubmit: (values) async => await api.updateProfile(values),
);
}
}
SmartFormController API #
| Member | Description |
|---|---|
setValue(id, value) |
Sets a single field's value |
setValues(Map<String, dynamic>) |
Sets multiple field values at once |
getValue(id) |
Reads a single field's current value |
values / visibleValues |
All values, or only values for currently visible fields |
submit() |
Triggers submission; returns a Future<void> that completes once validation and onSubmit have finished |
reset() |
Resets all fields to their initial values and clears errors |
clearErrors() |
Clears validation errors without changing values |
isDirty / isSubmitting / hasErrors |
Read-only state flags |
Calls made before the form mounts (setValue, setValues, submit) are
queued and automatically replayed once the form attaches — this makes the
"pre-fill from an API response that might resolve before first build" case
work without any extra bookkeeping on your end. A single controller can only
be attached to one mounted SmartForm at a time, and any use of a disposed
controller throws a clear StateError rather than failing silently.
Autosave & draft restore #
SmartForm(
schema: schema,
autosave: AutosaveConfig(
key: 'registration_draft',
debounce: const Duration(milliseconds: 500),
onDraftRestored: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Draft restored')),
);
},
),
onSubmit: (values) async => await api.register(values),
)
The draft is persisted on every debounced change and cleared automatically on
a successful submission. To inspect what was restored (rather than just
being notified that something was), read it from a SmartFormController
attached to the same form.
Theming #
SmartForm uses your ambient ThemeData by default. Override anything via FormTheme:
SmartForm(
schema: schema,
theme: FormTheme(
// Custom input decoration applied to all fields
inputDecoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
filled: true,
fillColor: Colors.grey.shade50,
),
// Per-field decoration override
inputDecorationBuilder: (base, field) => base.copyWith(
prefixIcon: field.type == FieldType.email
? const Icon(Icons.email_outlined)
: null,
),
// Button builders
submitButtonBuilder: (context, isLoading, onPressed) => ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(52)),
child: isLoading
? const CircularProgressIndicator.adaptive()
: const Text('Create account'),
),
// Wizard step indicator
activeStepColor: Colors.deepPurple,
showLinearProgress: true,
),
)
Custom field types #
Register any widget as a field type via FieldRegistry:
SmartForm(
schema: FormSchema(
fields: [
FieldSchema(
id: 'brand_color',
type: FieldType.text, // base type ignored — custom builder takes over
label: 'Brand color',
extra: {'defaultColor': '#5C6BC0'},
),
],
),
registry: buildDefaultRegistry(theme: const FormTheme())
..register(
FieldType.custom('color_picker'),
(context, field, state) => ColorPickerField(
label: field.label,
value: state.getValue(field.id) as Color?,
onChanged: (color) => state.setValue(field.id, color),
defaultColor: Color(
int.parse((field.extra['defaultColor'] as String)
.replaceAll('#', '0xFF')),
),
),
),
onSubmit: (values) async => print(values),
)
JSON schema reference #
All field types and conditions can be defined in JSON. Useful for:
- Remote form configuration (A/B test form variants without a release)
- No-code / low-code form builders
- Server-driven UI patterns
{
"title": "Contact us",
"fields": [
{
"id": "name",
"type": "text",
"label": "Your name",
"validators": [
{ "type": "required" },
{ "type": "min_length", "value": 2 }
]
},
{
"id": "email",
"type": "email",
"label": "Email address",
"validators": [
{ "type": "required" },
{ "type": "email" }
]
},
{
"id": "subject",
"type": "dropdown",
"label": "Subject",
"options": {
"type": "static",
"options": [
{ "value": "support", "label": "Technical support" },
{ "value": "billing", "label": "Billing" },
{ "value": "other", "label": "Other" }
]
},
"validators": [{ "type": "required" }]
},
{
"id": "order_id",
"type": "text",
"label": "Order ID",
"visible_when": {
"op": "equals",
"field": "subject",
"value": "billing"
}
},
{
"id": "message",
"type": "textarea",
"label": "Message",
"min_lines": 4,
"max_lines": 8,
"validators": [
{ "type": "required" },
{ "type": "min_length", "value": 20 }
]
}
]
}
JSON condition operators #
{ "op": "equals", "field": "country", "value": "India" }
{ "op": "not_equals", "field": "type", "value": "free" }
{ "op": "contains", "field": "roles", "value": "admin" }
{ "op": "greater_than", "field": "age", "value": 18 }
{ "op": "less_than", "field": "price", "value": 1000 }
{ "op": "is_empty", "field": "coupon" }
{ "op": "is_not_empty", "field": "company" }
{ "op": "and", "conditions": [ {...}, {...} ] }
{ "op": "or", "conditions": [ {...}, {...} ] }
{ "op": "not", "condition": { "op": "equals", "field": "x", "value": "y" } }
JSON validator types #
{ "type": "required", "message": "Custom message" }
{ "type": "min_length", "value": 8 }
{ "type": "max_length", "value": 200 }
{ "type": "email" }
{ "type": "phone" }
{ "type": "pattern", "pattern": "^\\d{6}$", "message": "Must be 6 digits" }
{ "type": "min_value", "value": 0 }
{ "type": "max_value", "value": 100 }
{ "type": "match_field", "field": "password", "message": "Passwords must match" }
Architecture overview #
flutter_form_builder_pro/
├── schema/ Pure Dart — no Flutter dependency — 100% unit testable
│ ├── FieldType Sealed enum of all field types
│ ├── FieldCondition Sealed class, 9 composable operators
│ ├── FieldValidator Sync + async, 10 built-ins + custom
│ ├── OptionsSource Static list or async loader with caching
│ ├── FieldSchema Full field descriptor, named constructors, JSON
│ └── FormSchema Root schema — flat or wizard, JSON roundtrip
│
├── engine/ Flutter (ChangeNotifier) — reactive form runtime
│ ├── FormStateNotifier Values, errors, dirty/touched, parallel validation
│ ├── StepController Wizard navigation, step skipping, progress
│ ├── FieldRegistry Type → widget builder map, custom type support
│ └── SmartFormController External handle — read, write, reset, submit
│
├── autosave/
│ ├── AutosaveConfig Storage key, debounce, restore callback
│ └── AutosaveEngine Persistence, restore-on-mount, clear-on-submit
│
├── theme/
│ └── FormTheme Full visual configuration, copyWith
│
└── widgets/ Flutter widgets — the visible layer
├── SmartForm Main entry point — flat + wizard modes
├── BaseFieldWrapper Shared label/error/helper layout
├── default_registry Wires all 14 built-in type mappings
├── fields/ One file per field widget
└── wizard/ StepIndicator + WizardNavigationBar
Comparison #
| flutter_form_builder_pro | flutter_form_builder | reactive_forms | |
|---|---|---|---|
| Schema-driven | ✅ | ❌ | ❌ |
| JSON schema | ✅ | ❌ | ❌ |
| Conditional fields | ✅ | ❌ | Manual |
| Async validation | ✅ | ✅ | ✅ |
| Multi-step wizard | ✅ | ❌ | ❌ |
| Step skip via condition | ✅ | ❌ | ❌ |
| Custom field types | ✅ | ✅ | ✅ |
| Dart 3 sealed classes | ✅ | ❌ | ❌ |
| External state management | Optional | Optional | Required |
Contributing #
Contributions are very welcome! Here is how to get started:
- Fork the repository on GitHub
- Clone your fork:
git clone https://github.com/Sarvesh223/flutter_form_builder_pro - Create a feature branch:
git checkout -b feat/my-feature - Write tests for your change — all PRs require test coverage
- Run tests:
flutter test - Run the analyzer:
flutter analyze - Open a pull request with a clear description of what and why
Development setup #
git clone https://github.com/Sarvesh223/flutter_form_builder_pro
cd flutter_form_builder_pro
flutter pub get
flutter test # run all tests
cd example && flutter run # run the example app
Reporting bugs #
Please open an issue and include:
- Flutter version (
flutter --version) - A minimal code snippet that reproduces the problem
- What you expected vs. what happened
Requesting features #
Feature requests are tracked as GitHub issues. Please check existing issues before opening a new one.
Sponsorship #
flutter_form_builder_pro is free and open source. If it saves you time on your projects,
consider sponsoring continued development:
Sponsors receive:
- Priority responses on issues and PRs
- Acknowledgement in the changelog and README
- Early access to upcoming features
License #
MIT © 2026 flutter_form_builder_pro contributors