formix 0.0.2 copy "formix: ^0.0.2" to clipboard
formix: ^0.0.2 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 Version

Full Documentation

An elite, type-safe, and ultra-reactive form engine for Flutter. Powered by Riverpod, Formix delivers lightning-fast performance and effortless memory management, whether you're building a simple contact form or a massive, multi-section enterprise dashboard.

πŸ“¦ Installation #

Add formix to your pubspec.yaml:

dependencies:
  formix: ^0.0.1
  flutter_riverpod: ^2.5.1

Or run:

flutter pub add formix

⚑ Quick Start (For Beginners) #

The fastest way to build a type-safe form in 3 minutes.

1. Define your fields #

final emailField = FormixFieldID<String>('email');
final ageField = FormixFieldID<int>('age');

2. Wrap your app in a Scope #

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

3. Build the Form #

Formix(
  child: Column(
    children: [
      RiverpodTextFormField(
        fieldId: emailField,
        decoration: InputDecoration(labelText: 'Email'),
      ),
      RiverpodNumberFormField(
        fieldId: ageField,
        decoration: InputDecoration(labelText: 'Age'),
      ),
      FormixBuilder(
        builder: (context, scope) => ElevatedButton(
          onPressed: () => scope.submit(
            onValid: (values) => print('Saving: $values'),
          ),
          child: Text('Submit'),
        ),
      ),
    ],
  ),
)

✨ Features #

  • πŸ”’ True Type Safety: No more map['key'] as String. Use FormixFieldID<T>.
  • πŸš€ Extreme Performance: Only the specific field widget rebuilds when its value changes.
  • πŸ—‘οΈ Zero Memory Leaks: Controllers are automatically disposed via Riverpod's autoDispose.
  • πŸš₯ Flexible Validation: Sync, Async, Debounced, and Cross-field rules.
  • πŸ—οΈ Lazy Sections: Built for massive forms. Register fields only when UI is built.
  • πŸ’Ύ Persistence: Built-in support for saving draft state automatically.
  • 🎯 UX First: Automated scrolling to errors, focus management, and navigation guards.
  • πŸ”‘ GlobalKey Access: Control forms from anywhere using GlobalKey<FormixState> (like FormBuilder).
  • πŸ”” Form Callbacks: React to changes with onChanged for auto-save and analytics.
  • 🎭 Multi-Form Support: Parallel and nested forms with complete state isolation.

πŸ“– Essential Guide #

Defining Field Configuration #

While Formix works with zero setup, you can define rules globally or locally using FormixFieldConfig.

Formix(
  fields: [
    FormixFieldConfig<String>(
      id: emailField,
      label: 'User Email',
      validator: (val) => val.contains('@') ? null : 'Invalid email',
      initialValue: 'guest@example.com',
    ),
  ],
  child: ...
)

Accessing Values & State #

Use FormixScope (via FormixBuilder or FormixWidget) for the cleanest API.

FormixBuilder(
  builder: (context, scope) {
    // Reactive: Rebuilds whenever 'age' changes
    final age = scope.watchValue(ageField);

    // Non-reactive: Get value without rebuilt
    final currentAge = scope.getValue(ageField);

    return Text('Age is $age');
  },
)

πŸ› οΈ Professional Guide #

1. Custom Field Implementation #

Extend FormixFieldWidget to create pixel-perfect custom inputs with zero boilerplate.

class CustomToggleField extends FormixFieldWidget<bool> {
  const CustomToggleField({required super.fieldId});

  @override
  Widget buildForm(BuildContext context, FormixScope scope) {
    final value = scope.watchValue(fieldId) ?? false;

    return SwitchListTile(
      value: value,
      onChanged: (v) => scope.setValue(fieldId, v),
      title: Text('Toggle Me'),
    );
  }
}

2. Data Loss Prevention #

Use FormixNavigationGuard to stop users from accidentally navigating away after they've spent time filling out a form.

Formix(
  child: FormixNavigationGuard(
    // Shows a default confirmation dialog if form is dirty
    child: MyFormBody(),
  ),
)

3. Sectional & Lazy Scaling #

For huge forms, use FormixSection. Fields are only registered when they enter the widget tree.

ListView(
  children: [
    FormixSection(
      fields: [ /* Section 1 Configs */ ],
      child: StepOneWidgets(),
    ),
    FormixSection(
      fields: [ /* Section 2 Configs */ ],
      child: StepTwoWidgets(),
    ),
  ],
)

4. Performance Monitoring #

During development, use FormixFieldPerformanceMonitor to ensure your custom widgets aren't rebuilding too often.

FormixFieldPerformanceMonitor<String>(
  fieldId: nameField,
  builder: (context, info, rebuildCount) {
    return Column(
      children: [
        Text('Rebuilds: $rebuildCount'),
        MyWidget(info.value),
      ],
    );
  },
)

4. Simplified Usage with Builders or Base Classes #

Custom widgets can leverage FormixScope for an effortless development experience. FormixScope provides high-performance reactive accessors and a powerful submit helper.

Using FormixBuilder

FormixBuilder(
  builder: (context, scope) {
    final isValid = scope.watchIsValid;
    final isSubmitting = scope.watchIsSubmitting;

    return ElevatedButton(
      onPressed: (isValid && !isSubmitting)
        ? () => scope.submit(onValid: (values) async => print(values))
        : null,
      child: Text(isSubmitting ? 'Submitting...' : 'Submit'),
    );
  },
)

Extending FormixWidget

class FormStatusPanel extends FormixWidget {
  const FormStatusPanel({super.key});

  @override
  Widget buildForm(BuildContext context, FormixScope scope) {
    // Rebuilds ONLY when 'name' or validation for 'email' changes
    final name = scope.watchValue(nameField);
    final emailError = scope.watchError(emailField);
    final isValidating = scope.watchIsValidating(emailField);

    return Column(
      children: [
        if (name != null) Text('Welcome, $name'),
        if (isValidating) const CircularProgressIndicator(),
        if (emailError != null) Text(emailError, style: TextStyle(color: Colors.red)),
      ],
    );
  }
}

5. External Form Control with GlobalKey #

Access and control your form from outside its widget tree using GlobalKey<FormixState> - perfect for AppBar actions, floating buttons, or external validation triggers.

class MyFormPage extends StatelessWidget {
  final _formKey = GlobalKey<FormixState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('User Profile'),
        actions: [
          IconButton(
            icon: Icon(Icons.save),
            onPressed: () {
              // Access form from anywhere!
              final controller = _formKey.currentState?.controller;
              final data = _formKey.currentState?.data;

              if (data?.isValid ?? false) {
                print('Saving: ${data?.values}');
                controller?.submit(
                  onValid: (values) async {
                    await saveToServer(values);
                  },
                );
              }
            },
          ),
        ],
      ),
      body: Formix(
        key: _formKey,
        initialValue: {'name': 'John', 'email': 'john@example.com'},
        child: Column(
          children: [
            RiverpodTextFormField(fieldId: nameField),
            RiverpodTextFormField(fieldId: emailField),
          ],
        ),
      ),
    );
  }
}

6. Form Change Callbacks #

React to any value change in your form with the onChanged callback - ideal for auto-save, analytics, or real-time validation.

Formix(
  initialValue: {'name': '', 'email': ''},
  onChanged: (values) {
    // Triggered whenever ANY field changes
    print('Form updated: $values');

    // Auto-save draft
    saveDraft(values);

    // Track analytics
    analytics.logFormProgress(values.length);
  },
  child: MyFormFields(),
)

7. Multiple Forms: Parallel & Nested #

Formix seamlessly handles multiple independent forms on the same screen, or nested forms for complex hierarchical data.

Parallel Forms (Independent)

Column(
  children: [
    // Form 1: User Info
    Formix(
      key: ValueKey('user_form'),
      initialValue: {'name': 'Alice'},
      child: Column(
        children: [
          Text('User Information'),
          RiverpodTextFormField(fieldId: nameField),
          FormixBuilder(
            builder: (context, scope) => ElevatedButton(
              onPressed: () => scope.submit(
                onValid: (values) => saveUser(values),
              ),
              child: Text('Save User'),
            ),
          ),
        ],
      ),
    ),

    Divider(),

    // Form 2: Company Info (completely independent)
    Formix(
      key: ValueKey('company_form'),
      initialValue: {'company': 'Acme Corp'},
      child: Column(
        children: [
          Text('Company Information'),
          RiverpodTextFormField(fieldId: companyField),
          FormixBuilder(
            builder: (context, scope) => ElevatedButton(
              onPressed: () => scope.submit(
                onValid: (values) => saveCompany(values),
              ),
              child: Text('Save Company'),
            ),
          ),
        ],
      ),
    ),
  ],
)

Nested Forms (Hierarchical)

// Outer form: Order details
Formix(
  initialValue: {'orderId': '12345', 'status': 'pending'},
  child: Column(
    children: [
      RiverpodTextFormField(fieldId: orderIdField),
      RiverpodTextFormField(fieldId: statusField),

      // Inner form: Shipping address (isolated scope)
      Formix(
        initialValue: {
          'street': '123 Main St',
          'city': 'New York',
          'zip': '10001',
        },
        child: FormixBuilder(
          builder: (context, scope) {
            // This scope only sees shipping fields
            return Column(
              children: [
                Text('Shipping Address'),
                RiverpodTextFormField(fieldId: streetField),
                RiverpodTextFormField(fieldId: cityField),
                RiverpodTextFormField(fieldId: zipField),
                ElevatedButton(
                  onPressed: () => scope.submit(
                    onValid: (address) => validateAddress(address),
                  ),
                  child: Text('Validate Address'),
                ),
              ],
            );
          },
        ),
      ),
    ],
  ),
)

Key Benefits:

  • βœ… Each form maintains its own state, validation, and dirty tracking
  • βœ… Nested forms can share field IDs without conflicts
  • βœ… Submit, reset, and validation are scoped to each form
  • βœ… Perfect for wizards, multi-step flows, or complex data entry

Multi-Step Wizard with Validation

Build complex multi-step forms with independent validation per step using GlobalKey for external control:

class RegistrationWizard extends StatefulWidget {
  @override
  State<RegistrationWizard> createState() => _RegistrationWizardState();
}

class _RegistrationWizardState extends State<RegistrationWizard> {
  int _currentStep = 0;

  // GlobalKeys for each step
  final _step1Key = GlobalKey<FormixState>();
  final _step2Key = GlobalKey<FormixState>();
  final _step3Key = GlobalKey<FormixState>();

  // Field IDs
  final nameField = FormixFieldID<String>('name');
  final emailField = FormixFieldID<String>('email');
  final addressField = FormixFieldID<String>('address');
  final cityField = FormixFieldID<String>('city');
  final termsField = FormixFieldID<bool>('terms');

  bool _canProceed() {
    final currentKey = [_step1Key, _step2Key, _step3Key][_currentStep];
    return currentKey.currentState?.data.isValid ?? false;
  }

  void _onStepContinue() {
    if (_canProceed()) {
      if (_currentStep < 2) {
        setState(() => _currentStep++);
      } else {
        _submitForm();
      }
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Please fix errors before continuing')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stepper(
      currentStep: _currentStep,
      onStepContinue: _onStepContinue,
      onStepCancel: () => setState(() => _currentStep--),
      steps: [
        Step(
          title: Text('Personal Info'),
          content: ProviderScope(
            child: Formix(
              key: _step1Key,
              fields: [
                FormixFieldConfig<String>(
                  id: nameField,
                  validator: (v) => v.isEmpty ? 'Required' : null,
                ),
                FormixFieldConfig<String>(
                  id: emailField,
                  validator: (v) => v.contains('@') ? null : 'Invalid email',
                ),
              ],
              child: Column(
                children: [
                  RiverpodTextFormField(
                    fieldId: nameField,
                    decoration: InputDecoration(labelText: 'Name'),
                  ),
                  RiverpodTextFormField(
                    fieldId: emailField,
                    decoration: InputDecoration(labelText: 'Email'),
                  ),
                ],
              ),
            ),
          ),
        ),
        Step(
          title: Text('Address'),
          content: ProviderScope(
            child: Formix(
              key: _step2Key,
              fields: [
                FormixFieldConfig<String>(
                  id: addressField,
                  validator: (v) => v.isEmpty ? 'Required' : null,
                ),
                FormixFieldConfig<String>(
                  id: cityField,
                  validator: (v) => v.isEmpty ? 'Required' : null,
                ),
              ],
              child: Column(
                children: [
                  RiverpodTextFormField(
                    fieldId: addressField,
                    decoration: InputDecoration(labelText: 'Street Address'),
                  ),
                  RiverpodTextFormField(
                    fieldId: cityField,
                    decoration: InputDecoration(labelText: 'City'),
                  ),
                ],
              ),
            ),
          ),
        ),
        Step(
          title: Text('Confirm'),
          content: ProviderScope(
            child: Formix(
              key: _step3Key,
              fields: [
                FormixFieldConfig<bool>(
                  id: termsField,
                  validator: (v) => v ? null : 'Must accept terms',
                ),
              ],
              child: FormixBuilder(
                builder: (context, scope) {
                  final accepted = scope.watchValue(termsField) ?? false;
                  return CheckboxListTile(
                    title: Text('I accept the terms and conditions'),
                    value: accepted,
                    onChanged: (v) => scope.setValue(termsField, v ?? false),
                  );
                },
              ),
            ),
          ),
        ),
      ],
    );
  }
}

See the full example: Check example/lib/ui/multi_step_form/multi_step_form_page.dart for a complete 4-step registration form with validation, progress tracking, and data collection.

8. Dynamic Form Arrays #

Manage lists of dynamic inputs easily with FormixArray.

final friendsArray = FormixArrayID<String>('friends');

FormixArray<String>(
  id: friendsArray,
  builder: (context, index, friendId, scope) {
    return Row(
      children: [
        Expanded(child: RiverpodTextFormField(fieldId: friendId)),
        IconButton(
          icon: Icon(Icons.delete),
          onPressed: () => scope.removeArrayItemAt(friendsArray, index),
        ),
      ],
    );
  },
)

// Add new items via scope
scope.addArrayItem(friendsArray, 'New Friend');

🧠 Advanced Features #

1. Lazy Loading & Sectional Forms #

For massive forms (100+ fields), you can organize fields into sections using FormixSection. This allows:

  • Optimization: Fields are only registered when the section is built (e.g., when scrolled into view in a ListView).
  • Organization: Cleanly group logical parts of your form.
  • Dynamic Forms: Easily add/remove entire sets of fields based on user interaction.
Formix(
  child: ListView(
    children: [
      // Standard header
      const Text('Profile Information'),

      // Registered immediately
      FormixSection(
        fields: [ firstNameConfig, lastNameConfig ],
        child: Column(children: [ ... ]),
      ),

      const SizedBox(height: 1000), // Long gap

      // Only registered when user scrolls down
      FormixSection(
        fields: [ bioConfig, websiteConfig ],
        keepAlive: true, // Keep values even if scrolled out of view
        child: ProfileBioSection(),
      ),
    ],
  ),
)

2. Cross-Field Dependencies #

Use FormixDependentField to conditionally render UI based on other field values. This is much more efficient than rebuilding the whole form.

// Only show the "Spouse Name" field if "Marital Status" is "Married"
FormixDependentField<String>(
  fieldId: maritalStatusField,
  builder: (context, status) {
    if (status == 'Married') {
      return RiverpodTextFormField(
        fieldId: spouseNameField,
        decoration: const InputDecoration(labelText: 'Spouse Name'),
      );
    }
    return const SizedBox.shrink();
  },
)

3. Programmatic Control #

Directly control focus and scrolling within your form through the FormixScope.

FormixBuilder(
  builder: (context, scope) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () => scope.focusField(emailField),
          child: Text('Jump to Email'),
        ),
        ElevatedButton(
          onPressed: () => scope.focusFirstError(),
          child: Text('Fix Errors'),
        ),
        ElevatedButton(
          onPressed: () => scope.scrollToField(lastFieldId),
          child: Text('Scroll to Bottom'),
        ),
      ],
    );
  },
)

2. State Persistence #

Automatically save form progress to local storage (or any other source) so users don't lose data on app restart or crash.

Implement the simple FormixPersistence interface:

class MyPrefsPersistence implements FormixPersistence {
  @override
  Future<void> saveFormState(String formId, Map<String, dynamic> values) async {
    // Save to SharedPreferences / Hive / Database
  }

  // ... implement getSavedState and clearSavedState
}

Then attach it to your form:

Formix(
  formId: 'registration_wizard', // Unique ID for this form
  persistence: MyPrefsPersistence(),
  child: ...
)

πŸ’‘ Pro Tips: Usability & State Management #

Enhance your form's resilience and user experience with these three powerful features.

1. keepAlive: Preserving State Across Navigation #

By default, Formix automatically disposes of its state when the widget is unmounted to save memory. However, in multi-step forms (like wizards or tabs) where the user navigates back and forth, you don't want to lose their data.

Set keepAlive: true to preserve the form state in memory even if the widget is temporarily removed from the tree.

Formix(
  keepAlive: true, // Prevents auto-disposal
  child: MyFormStep(),
)

2. formId: Identification for Persistence #

When using FormixPersistence to save drafts, formId is required. It acts as the unique key to store and retrieve data for this specific form instance.

Formix(
  formId: 'user_onboarding_v1', // Must be unique across the app
  persistence: MyLocalPersistence(),
  child: ...
)

3. Off-Stage Widget Registering #

In complex layouts like IndexedStack, PageView, or tabs, some field widgets might be "off-stage" (not yet built). By default, Formix only knows about fields that have been rendered. This causes issues if you try to validate() a form where required fields haven't been built yetβ€”they effectively don't exist.

To fix this, pre-register your fields by passing them to the Formix constructor. This ensures the controller knows about them immediately, allowing for full validation even if the UI is hidden.

final nameField = FormixFieldID<String>('name');
final bioField = FormixFieldID<String>('bio');

Formix(
  // Register fields upfront so validation works even if widgets aren't built
  fields: [
    FormixFieldConfig<String>(id: nameField, validator: (v) => v.isNotEmpty ? null : 'Required'),
    FormixFieldConfig<String>(id: bioField, validator: (v) => v.isNotEmpty ? null : 'Required'),
  ],
  child: IndexedStack(
    index: _currentIndex,
    children: [
      // Step 1: Visible
      RiverpodTextFormField(fieldId: nameField),

      // Step 2: Hidden (Off-stage), but still validated thanks to 'fields' above!
      RiverpodTextFormField(fieldId: bioField),
    ],
  ),
)

🚦 Error Handling & Advanced UI #

Focus & Error Management #

Guide your users directly to what needs fixing.

scope.submit(
  onInvalid: (errors) {
    // Automatically focus the first field with an error
    // and scroll it into view.
    scope.focusFirstError();
  },
  onValid: (values) async {
    try {
      await api.save(values);
    } catch (e) {
      // Manually set an error from the server
      scope.setFieldError(emailField, 'Account already exists');
    }
  }
)

Handling Async Validation #

Formix automatically handles the "Validating" state. You can customize the loading UI:

FormixFieldConfig<String>(
  id: usernameField,
  asyncValidator: (val) => checkRepo(val),
  // debouncing is default to 300ms
  debounceDuration: Duration(seconds: 1),
)

Common Pitfalls (Troubleshooting) #

Issue Solution
"No Formix found" Ensure all fields are descendants of a Formix widget.
"No Material widget found" RiverpodTextFormField and others require a Material / Scaffold ancestor.
Field doesn't rebuild Ensure you are using scope.watchValue(id) or ref.watch(fieldValueProvider(id)).
Custom field not registering If using FormixFieldWidget, it registers itself. If building from scratch, use FormixSection or controller.registerField.

Widgets #

  • Formix: Root widget context.
  • FormixBuilder: Builder widget with controller and state.
  • FormixWidget: Base class for custom form widgets.
  • FormixSection: Lazy field registration and sectional organization.
  • RiverpodTextFormField: Text input.
  • RiverpodNumberFormField: Numeric input (int/double).
  • RiverpodCheckboxFormField: Boolean checkbox.
  • RiverpodDropdownFormField: Selection from list.
  • FormixDependentField: Reactive builder for dependencies.
  • RiverpodFormStatus: Debug/Status display.

Classes #

  • FormixFieldID<T>: Typed identifier key.
  • FormixFieldConfig<T>: Configuration (validator, label, initialValue, etc).
  • FormixState: The immutable state containing all field values and errors.
  • RiverpodFormController: The brain ensuring state management.

🀝 Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

2
likes
150
points
55
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

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, flutter_riverpod, index_generator

More

Packages that depend on formix