formix 0.0.5 copy "formix: ^0.0.5" to clipboard
formix: ^0.0.5 copied to clipboard

An elite, type-safe, and ultra-reactive form engine for Flutter powered by Riverpod.

Formix ๐Ÿš€ #

Formix Logo

Pub License Code Coverage Tests

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 #

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 all FormixAsyncField widgets to finish loading before executing your onValid callback.


๐ŸŽฎ 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 (int or double).
  • 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: Access FormixScope for 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 #

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 select to 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 LoggingFormAnalytics to 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

4
likes
0
points
561
downloads

Publisher

verified publishershreeman.dev

Weekly Downloads

An elite, type-safe, and ultra-reactive form engine for Flutter powered by Riverpod.

Homepage
Repository (GitHub)
View/report issues

Topics

#form #validation #riverpod #type-safe #state-management

License

unknown (license)

Dependencies

collection, flutter, flutter_riverpod, index_generator, meta, stream_transform

More

Packages that depend on formix