formix 0.0.5
formix: ^0.0.5 copied to clipboard
An elite, type-safe, and ultra-reactive form engine for Flutter powered by Riverpod.
Formix ๐ #
An elite, type-safe, and ultra-reactive form engine for Flutter.
Powered by Riverpod, Formix delivers lightning-fast performance, zero boilerplate, and effortless state management. Whether it's a simple login screen or a complex multi-step wizard, Formix scales with you.
๐ Table of Contents #
- ๐ฆ Installation
- โก Quick Start
- ๐ฎ Usage Guide
- ๐ฎ Core Concepts
- ๐งฑ Widget Reference
- ๐ฅ Validation
- ๐น๏ธ Controlling the Form
- ๐งช Advanced Features
- โก Performance
- ๐ Analytics & Debugging
๐ฆ Installation #
flutter pub add formix
โก Quick Start #
1. Define Fields #
Always use FormixFieldID<T> for type-safe field identification.
final emailField = FormixFieldID<String>('email');
final ageField = FormixFieldID<int>('age');
2. Build Form #
Formix(
child: Column(
children: [
FormixTextFormField(fieldId: emailField),
FormixNumberFormField(fieldId: ageField),
FormixBuilder(
builder: (context, scope) => ElevatedButton(
onPressed: scope.watchIsValid ? () => scope.submit(onValid: _submit) : null,
child: Text('Submit'),
),
),
],
),
)
๐ฎ Usage Guide #
Basic Login Form #
Here is a complete, real-world example of a login form with validation and loading state.
class LoginForm extends StatelessWidget {
static final emailField = FormixFieldID<String>('email');
static final passwordField = FormixFieldID<String>('password');
@override
Widget build(BuildContext context) {
return Formix(
child: Column(
children: [
FormixTextFormField(
fieldId: emailField,
decoration: InputDecoration(labelText: 'Email'),
validator: FormixValidators.string().required().email().build(),
),
FormixTextFormField(
fieldId: passwordField,
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
validator: FormixValidators.string().required().minLength(8).build(),
),
SizedBox(height: 20),
FormixBuilder(
builder: (context, scope) {
if (scope.watchIsSubmitting) {
return CircularProgressIndicator();
}
return ElevatedButton(
onPressed: scope.watchIsValid
? () => scope.submit(onValid: (values) async {
await authService.login(
values[emailField.key],
values[passwordField.key]
);
})
: null,
child: Text('Login'),
);
},
),
],
),
);
}
}
Using Dropdowns & Checkboxes #
final roleField = FormixFieldID<String>('role');
final termsField = FormixFieldID<bool>('terms');
// ... inside Formix
FormixDropdownFormField<String>(
fieldId: roleField,
items: [
DropdownMenuItem(value: 'admin', child: Text('Admin')),
DropdownMenuItem(value: 'user', child: Text('User')),
],
decoration: InputDecoration(labelText: 'Select Role'),
),
FormixCheckboxFormField(
fieldId: termsField,
title: Text('I agree to terms'),
validator: (val) => val == true ? null : 'Required',
),
Async Data & Dependent Dropdowns #
Use FormixAsyncField and FormixDependentAsyncField for fields that require asynchronous data fetching or depend on other field values. They automatically manage loading states, race conditions, and integrate with the form's isPending status.
FormixDependentAsyncField
Perfect for parent-child field relationships (e.g., Country -> City).
FormixDependentAsyncField<List<String>, String>(
fieldId: cityOptionsField,
dependency: countryField,
resetField: cityField, // Automatically clear selected city when country changes
future: (country) => api.fetchCities(country),
keepPreviousData: true,
loadingBuilder: (context) => LinearProgressIndicator(),
builder: (context, state) {
final cities = state.asyncState.value ?? [];
return FormixDropdownFormField<String>(
fieldId: cityField,
items: cities.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
decoration: InputDecoration(labelText: 'City'),
);
},
)
FormixAsyncField
Use this when you have a future that doesn't depend on other form fields.
FormixAsyncField<List<String>>(
fieldId: categoryField,
future: api.fetchCategories(),
builder: (context, state) {
return FormixDropdownFormField<String>(
fieldId: categoryField,
items: (state.asyncState.value ?? []).map(...).toList(),
);
},
)
Computed Fields & Transformers #
Synchronize or transform data between fields automatically.
FormixFieldTransformer
Synchronously maps a value from a source field to a target field.
FormixFieldTransformer<String, int>(
sourceField: bioField,
targetField: bioLengthField,
transform: (bio) => bio?.length ?? 0,
)
FormixFieldAsyncTransformer
Asynchronously transforms values with built-in debounce and race condition protection.
FormixFieldAsyncTransformer<String, String>(
sourceField: promoCodeField,
targetField: discountLabelField,
debounce: Duration(milliseconds: 500),
transform: (code) => api.verifyPromoCode(code),
)
Pro Tip:
controller.submit()automatically waits for allFormixAsyncFieldwidgets to finish loading before executing youronValidcallback.
๐ฎ Core Concepts #
The Three Pillars #
| Pattern | Best For | Usage |
|---|---|---|
| Reactive UI | Updating buttons, labels, or visibility. | FormixBuilder(builder: (c, scope) => ...) |
| External Control | Logic outside the widget tree (AppBar buttons). | ref.read(formControllerProvider(...).notifier) |
| Side Effects | Navigation, Snackbars, Logging. | FormixListener |
Side Effects (FormixListener) #
Use FormixListener to execute one-off actions (like showing a dialog, navigation, or logging) in response to state changes. It does not rebuild the UI.
FormixListener(
formKey: _formKey,
listener: (context, state) {
if (state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Form has ${state.errorCount} errors!')),
);
}
},
child: Formix(key: _formKey, ...),
)
๐งฑ Widget Reference #
Standard Fields #
Formix includes high-performance widgets out of the box:
FormixTextFormField: Full-featured text input with auto-validation and focus management.FormixNumberFormField<T extends num>: Type-safe numeric input (intordouble).FormixCheckboxFormField: Boolean selection.FormixDropdownFormField<T>: Type-safe generic dropdown for selections.
Form Organization #
Manage complex hierarchies and performance:
| Widget | Purpose | Usage |
|---|---|---|
FormixGroup |
Namespaces fields (e.g. user.address). |
FormixGroup(prefix: 'address', child: ...) |
FormixArray<T> |
Manage dynamic lists of inputs. | FormixArray(id: FormixArrayID('items'), ...) |
FormixSection |
Persists data when swiping away (wizard/tabs). | FormixSection(keepAlive: true, ...) |
FormixFieldRegistry |
Lazy-loads fields only when mounted. | FormixFieldRegistry(fields: [...], ...) |
Reactive UI & Logic #
Make your forms "alive":
FormixBuilder: AccessFormixScopefor reactive UI components (buttons, progress bars).FormixDependentAsyncField<T, D>: Manages asynchronous data fetching based on a dependency. Reduce boilerplate for parent-child relationships.FormixDependentField<T>: Only rebuilds when a specific field changes.FormixDependentField<bool>( fieldId: hasPetField, builder: (context, hasPet) => hasPet ? PetForm() : SizedBox(), )FormixFieldSelector: Fine-grained selection of field state changes (value vs validation).FormixFieldDerivation: Computes values automatically from multiple dependencies (e.g.,Total = Price * Qty).FormixFieldTransformer: Type-safe 1-to-1 transformation between two fields (e.g.,String->int).FormixFieldTransformer<String, int>( sourceField: textId, targetField: lengthId, transform: (text) => text?.length ?? 0, )FormixFieldAsyncTransformer: Async 1-to-1 transformation with built-in debounce (e.g., fetching a user profile from an ID).FormixFieldAsyncTransformer<String, String>( sourceField: userIdField, targetField: userNameField, debounce: Duration(milliseconds: 500), transform: (id) async => await fetchUser(id), )FormixFormStatus: Pre-built dashboard showing validity, dirty state, and submission info.
Headless & Custom Widgets #
100% UI control with zero state-management headache.
FormixRawFormField<T> (Headless)
Perfect for using third-party UI libraries (like Shadcn or Material).
FormixRawFormField<String>(
fieldId: nameField,
builder: (context, state) => MyInput(
value: state.value,
error: state.validation.errorMessage,
onChanged: state.didChange,
),
)
FormixFieldWidget (Base Class)
Extend this to build your own reusable Formix-enabled components. It handles all controller wiring and focus management automatically. See also FormixFieldTextMixin for text fields.
FormixWidget
Base class for non-field components (like summaries) that need access to FormixScope.
๐ฅ Validation #
Fluent API (FormixValidators) #
Define readable, type-safe validation rules. Use .build() for synchronous and .buildAsync() for asynchronous rules.
// String Validation
FormixValidators.string()
.required('Email is mandatory')
.email('Invalid format')
.minLength(6)
.pattern(RegExp(r'...'))
.build()
// Number Validation
FormixValidators.number<int>()
.required()
.positive()
.min(18, 'Must be an adult')
.max(99)
.build()
Async Validation #
Async validators are debounced automatically to optimize server performance.
FormixFieldConfig(
id: usernameField,
asyncValidator: (val) async => await checkAvailability(val) ? null : 'Taken',
debounceDuration: Duration(milliseconds: 500),
)
๐น๏ธ Controlling the Form #
Accessing Formix #
There are three ways to access the form controller, depending on your context:
1. Inside the UI (FormixBuilder)
Best for reactive UI updates (buttons, visibility).
FormixBuilder(
builder: (context, scope) {
// scope.controller gives you full access
return ElevatedButton(onPressed: scope.reset);
}
)
2. Using GlobalKey (External Access)
Best for specialized use cases where you need to control the form from a completely different part of the tree.
final _formKey = GlobalKey<FormixState>();
// ... Formix(key: _formKey, ...)
void submitFromAppBar() {
_formKey.currentState?.controller.submit();
}
3. Using Riverpod (WidgetRef)
Best for complex logic, side effects, or extracting logic to separate providers.
// Reading properties
final isValid = ref.watch(formControllerProvider(param).select((s) => s.isValid));
// Executing actions
ref.read(formControllerProvider(param).notifier).reset();
๐ฎ Controller API Reference #
The FormixController is your command center.
State Updates
| Method | Description |
|---|---|
getValue(id) |
Retrieves the field value as T?. Supports smart fallback for unregistered fields. |
requireValue(id) |
Retrieves the field value as T and throws a StateError if null. |
setValue(val) |
Updates the field value. |
setValues(updates) |
Updates multiple field values at once (Returns FormixBatchResult). |
applyBatch(batch) |
Updates using a type-safe FormixBatch builder. |
reset() |
Resets all fields to initial values. |
resetField(id) |
Resets a specific field. |
markAsDirty(id) |
Manually marks a field as dirty. |
Validation
| Method | Description |
|---|---|
validate() |
Triggers validation for all fields. |
validateField(id) |
Validates a single field. |
setFieldError(id, msg) |
Sets an external error (e.g. from backend). |
Focus & Navigation
| Method | Description |
|---|---|
focusField(id) |
Moves focus to the specified field. |
focusFirstError() |
Automatically scrolls to the first invalid field. |
Advanced
| Method | Description |
|---|---|
undo() / redo() |
Navigates history stack. |
snapshot() |
Creates a restoration point. |
bindField(id, target) |
Syncs two fields together. |
Global Validation Mode #
You can control when validation is triggered globally for the entire form or override it per field.
Formix(
// Global mode for all fields
autovalidateMode: FormixAutovalidateMode.onUserInteraction,
child: Column(
children: [
FormixTextFormField(
fieldId: nameField,
// Uses global onUserInteraction by default
validator: (v) => v!.isEmpty ? 'Error' : null,
),
FormixTextFormField(
fieldId: pinField,
// Overrides global mode for this specific field
validationMode: FormixAutovalidateMode.always,
validator: (v) => v!.length < 4 ? 'Too short' : null,
),
],
),
| Mode | Behavior |
| :--- | :--- |
| `always` | Validates immediately on mount and every change. |
| `onUserInteraction` | (Default) Validates only after the first change/interaction. |
| `disabled` | Validation only happens when `validate()` or `submit()` is called. |
| `onBlur` | Validation only happens when the field loses focus. |
| `auto` | Per-field default. Inherits from the global `Formix.autovalidateMode`. |
### Cross-Field Validation
Validate fields based on the state of other fields.
```dart
FormixFieldConfig(
id: confirmField,
crossFieldValidator: (value, state) {
if (value != state.getValue(passwordField)) return 'No match';
return null;
},
)
๐งช Advanced Features #
Navigation Guard #
Prevent users from losing work when navigation occurs.
FormixNavigationGuard(
onPopInvoked: (didPop, isDirty) {
if (isDirty && !didPop) {
showDialog(
context: context,
builder: (c) => AlertDialog(
title: Text('Discard changes?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(c),
child: Text('Cancel')
),
TextButton(
onPressed: () {
Navigator.pop(c); // Close dialog
Navigator.pop(context); // Pop screen
},
child: Text('Discard')
),
],
),
);
}
},
child: Formix(...),
)
Persistence #
Auto-save form state to local storage (e.g., SharedPreferences).
class MyFormPersistence extends FormixPersistence {
@override
Future<void> saveFormState(String formId, Map<String, dynamic> state) async {
await prefs.setString(formId, jsonEncode(state));
}
@override
Future<Map<String, dynamic>?> loadFormState(String formId) async {
final str = prefs.getString(formId);
return str != null ? jsonDecode(str) : null;
}
}
// Usage
Formix(
formId: 'user_profile',
persistence: MyFormPersistence(),
...
)
Undo/Redo #
History is tracked automatically.
FormixBuilder(
builder: (context, scope) => Row(
children: [
IconButton(
icon: Icon(Icons.undo),
onPressed: scope.canUndo ? scope.undo : null,
),
IconButton(
icon: Icon(Icons.redo),
onPressed: scope.canRedo ? scope.redo : null,
),
],
),
)
Robust Bulk Updates & Type Safety #
Formix provides a dedicated API for handling large-scale data updates with built-in safety.
Type-Safe Batching (FormixBatch)
Build a collection of updates with strict compile-time type checking using the fluent API.
final batch = FormixBatch()
..setValue(emailField).to('user@example.com')
..setValue(ageField).to(25); // Guaranteed lint enforcement
final result = controller.applyBatch(batch);
if (!result.success) {
print(result.errors); // Access detailed error map
}
Robust Error Handling (FormixBatchResult)
Updates no longer crash on type mismatches or missing fields. They return a result object containing:
updatedFields: Successfully updated keys.typeMismatches: Map of field keys to error messages.missingFields: Fields provided but not registered in the form.
๐จโ๐ณ Cookbook #
Multi-step Form (Wizard) #
Easily manage multi-step forms by conditionally rendering fields. Formix preserves state for off-screen fields automatically.
int currentStep = 0;
Formix(
child: Column(
children: [
if (currentStep == 0) ...[
FormixTextFormField(fieldId: emailField, label: 'Email'),
ElevatedButton(onPressed: () => setState(() => currentStep = 1), child: Text('Next')),
] else ...[
FormixTextFormField(fieldId: passwordField, label: 'Password'),
ElevatedButton(onPressed: () => controller.submit(...), child: Text('Submit')),
],
],
),
)
Dependent Fields #
Fields that update based on other fields' values.
FormixFieldConfig(
id: cityField,
dependsOn: [countryField],
validator: (val, data) {
final country = data.getValue(countryField);
if (country == 'USA' && val == 'London') return 'London is not in USA';
return null;
},
)
Complex Object Array #
Managing a list of complex objects (e.g., a list of addresses).
FormixArray(
id: addressesField,
itemBuilder: (context, index, itemId, scope) => FormixGroup(
prefix: 'address_$index',
child: Column(
children: [
FormixTextFormField(fieldId: streetField, label: 'Street'),
FormixTextFormField(fieldId: zipField, label: 'Zip Code'),
],
),
),
)
Custom Field Implementation #
Create your own form fields by extending FormixFieldWidget.
class MyColorPicker extends FormixFieldWidget<Color> {
const MyColorPicker({super.key, required super.fieldId});
@override
FormixFieldWidgetState<Color> createState() => _MyColorPickerState();
}
class _MyColorPickerState extends FormixFieldWidgetState<Color> {
@override
Widget build(BuildContext context) {
return ColorTile(
color: value ?? Colors.blue,
onTap: () => didChange(Colors.red), // Updates form state
);
}
}
โก Performance #
Formix is engineered for massive scale.
- Granular Rebuilds: Uses
selectto only rebuild exact widgets that change. - O(1) Updates: Field updates are constant time, regardless of form size.
- Scalability: Tested with 5000+ active fields maintaining 60fps interaction.
- Lazy Evaluation: Validation and dependency chains are optimized to run only when necessary.
Stress Test Results (M1 Pro) #
- 1000 Fields Mount: <10ms
- Bulk Updates: ~50ms for 1000 fields. Single frame execution for
setValues. - Dependency Scale: ~160ms for 100,000 dependents. Ultra-fast traversal for deep chains.
- Memory Efficient: Uses lazy-cloning and shared validation contexts to minimize GC pressure and O(N) overhead during validation.
๐ Analytics & Debugging #
- Logging: Enable
LoggingFormAnalyticsto see every value change and validation event in the console. - DevTools: The Formix DevTools extension has been completely redesigned for v0.0.5! Inspect the form state tree, visualize dependency graphs, monitor validation performance, and edit field values in real-time with a premium tabbed interface. Available now on the DevTools sidebar.
Built with โค๏ธ for the Flutter Community