fifty_forms 0.2.1 copy "fifty_forms: ^0.2.1" to clipboard
fifty_forms: ^0.2.1 copied to clipboard

Production-ready form building with validation, multi-step wizards, and draft persistence for the Fifty Flutter Kit.

Fifty Forms #

pub package License: MIT

Full validation pipeline for Flutter forms -- you provide the layout, we handle the state.

25 built-in validators, async debounce validation, multi-step wizards with custom navigation, draft persistence that survives app kills, and optional builder callbacks that let you replace the navigation buttons, progress indicator, error summary, and submit button individually. Part of Fifty Flutter Kit.

Home Login Form Registration Multi-Step

Why fifty_forms #

  • Full validation pipeline, you build the UI -- FiftyFormController handles field registration, 25 built-in validators, async debounce validation, and draft persistence; you provide the layout.
  • Wizard forms with custom everything -- FiftyMultiStepForm comes with step validation, progress tracking, and navigation; replace the navigation buttons, progress display, error summary, and submit button individually via optional builders.
  • Async validators with debounce -- AsyncCustom<String> debounces server-side checks (username availability, email exists) so API calls fire only when the user pauses typing.
  • Draft persistence that survives app kills -- DraftManager auto-saves form state to GetStorage with configurable debounce; restore drafts on next open with a single hasDraft() check.

Installation #

dependencies:
  fifty_forms: ^0.2.1

For Contributors #

dependencies:
  fifty_forms:
    path: ../fifty_forms

Dependencies: fifty_tokens, fifty_theme, fifty_ui, fifty_storage, get_storage


Quick Start #

final controller = FiftyFormController(
  initialValues: {'email': '', 'password': ''},
  validators: {
    'email': [Required(), Email()],
    'password': [Required(), MinLength(8)],
  },
);

Column(
  children: [
    FiftyTextFormField(
      name: 'email',
      controller: controller,
      label: 'Email',
      keyboardType: TextInputType.emailAddress,
    ),
    FiftyTextFormField(
      name: 'password',
      controller: controller,
      label: 'Password',
      obscureText: true,
    ),
    FiftySubmitButton(
      controller: controller,
      label: 'LOGIN',
      onPressed: () => controller.submit((values) async {
        await api.login(values['email'], values['password']);
      }),
    ),
  ],
)

Architecture #

fifty_forms
+-- core/
|   +-- FiftyFormController   # Central state manager
|   +-- FieldState            # Immutable per-field state
+-- validators/
|   +-- Validator             # Sync validator base
|   +-- AsyncValidator        # Async validator with debounce
|   +-- Built-ins             # Required, Email, MinLength, etc.
+-- fields/
|   +-- FiftyTextFormField    # fifty_ui field wrappers
|   +-- FiftyDropdownFormField
|   +-- FiftyCheckboxFormField (+ others)
+-- widgets/
|   +-- FiftyForm             # Form container
|   +-- FiftySubmitButton     # Submit with loading state
|   +-- FiftyMultiStepForm    # Wizard container
|   +-- FiftyFormArray        # Dynamic repeating fields
|   +-- FiftyValidationSummary
+-- models/
|   +-- FormStep              # Step definition for wizards
|   +-- FormStatus            # Form lifecycle enum
+-- persistence/
    +-- DraftManager          # Auto-save via GetStorage

Core Components #

Component Description
FiftyFormController Central state manager: values, validation, submission
FieldState<T> Immutable container for a single field's state
FormStatus Enum: idle, validating, submitting, submitted, error
Validator Composable synchronous validator base class
AsyncValidator Asynchronous validator with debounce support
DraftManager Persists and restores form drafts via GetStorage
FiftyMultiStepForm Wizard-style multi-step form widget
FiftyFormArray Dynamic add/remove repeating field groups

Customization #

Every form UI widget accepts an optional builder callback. When provided, the builder replaces the default FDL rendering while the widget retains ownership of controller listening, state computation, and animations.

Custom Navigation Buttons #

Replace the default back/next/complete buttons on FiftyMultiStepForm:

FiftyMultiStepForm(
  controller: controller,
  steps: mySteps,
  stepBuilder: (context, index, step) => buildStepContent(index),
  onComplete: (values) => api.createUser(values),
  navigationBuilder: (isFirstStep, isLastStep, isSubmitting, onNext, onPrevious) {
    return Row(
      children: [
        if (!isFirstStep)
          TextButton(onPressed: onPrevious, child: const Text('Back')),
        const Spacer(),
        ElevatedButton(
          onPressed: isSubmitting ? null : onNext,
          child: Text(isLastStep ? 'Submit' : 'Continue'),
        ),
      ],
    );
  },
)

Custom Submit Button #

Replace the default FiftyButton on FiftySubmitButton:

FiftySubmitButton(
  controller: controller,
  label: 'SAVE',
  onPressed: () => controller.submit(save),
  buttonBuilder: (isLoading, isDisabled, onPressed, label) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      child: ElevatedButton(
        onPressed: onPressed,
        child: isLoading
            ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
            : Text(label),
      ),
    );
  },
)

Custom Validation Summary #

Replace the default error card on FiftyValidationSummary:

FiftyValidationSummary(
  controller: controller,
  contentBuilder: (errors, onFieldTap) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: errors.entries.map((e) {
        return GestureDetector(
          onTap: onFieldTap != null ? () => onFieldTap(e.key) : null,
          child: Text('${e.key}: ${e.value}', style: const TextStyle(color: Colors.red)),
        );
      }).toList(),
    );
  },
)

Custom Progress Indicator #

Replace the default step circles on FiftyFormProgress:

FiftyFormProgress(
  currentStep: 2,
  totalSteps: 4,
  stepLabels: ['Account', 'Profile', 'Preferences', 'Review'],
  contentBuilder: (currentStep, totalSteps, stepLabels) {
    return LinearProgressIndicator(value: currentStep / totalSteps);
  },
)

Builder Signatures #

Widget Builder parameter Callback signature
FiftyMultiStepForm navigationBuilder Widget Function(bool isFirstStep, bool isLastStep, bool isSubmitting, VoidCallback onNext, VoidCallback onPrevious)
FiftySubmitButton buttonBuilder Widget Function(bool isLoading, bool isDisabled, VoidCallback? onPressed, String label)
FiftyValidationSummary contentBuilder Widget Function(Map<String, String> errors, void Function(String fieldName)? onFieldTap)
FiftyFormProgress contentBuilder Widget Function(int currentStep, int totalSteps, List<String>? stepLabels)

All builders are optional. Omit them to use the default FDL UI.


API Reference #

FiftyFormController #

The central state manager for forms. Handles field registration, value tracking, validation (sync and async), and form submission.

final controller = FiftyFormController(
  initialValues: {'name': '', 'age': 0},
  validators: {
    'name': [Required(), MinLength(2)],
    'age': [Required(), Min(18)],
  },
  onValidationChanged: (isValid) => print('Valid: $isValid'),
);

// Get/set values
controller.setValue('name', 'John');
final name = controller.getValue<String>('name');

// Check state
controller.isValid;      // All fields valid?
controller.isDirty;      // Any field changed?
controller.isValidating; // Async validation running?

// Field operations
controller.registerField('newField', initialValue: '');
controller.unregisterField('fieldToRemove');
controller.markTouched('name');
controller.markAllTouched();

// Actions
await controller.validate();          // Validate all fields
await controller.validateField('email'); // Validate single field
controller.clearErrors();             // Clear all validation errors
controller.reset();                   // Reset to initial values
controller.clear();                   // Clear all values

// Submit
await controller.submit((values) async {
  await api.save(values);
});

// Cleanup
controller.dispose();

Key Properties:

Property Type Description
status FormStatus Current form lifecycle status
isValid bool All fields valid and not validating
isDirty bool Any field changed from initial value
isValidating bool Async validation in progress
values Map<String, dynamic> All current field values
errors Map<String, String> All fields with errors
fieldNames List<String> All registered field names

FieldState #

Immutable state container for a form field. Tracks the current value, validation error, and interaction state.

class FieldState<T> {
  final T? value;           // Current value
  final String? error;      // Validation error
  final bool isTouched;     // Field has been focused
  final bool isDirty;       // Value differs from initial
  final bool isValidating;  // Async validation running

  bool get isValid => error == null && !isValidating;
  bool get hasError => error != null;
}

// Usage
final state = controller.getFieldState('email');
if (state.isTouched && state.hasError) {
  print(state.error);
}

FormStatus #

Enum representing the form lifecycle status:

Status Description
idle Default state, ready for input
validating Validation in progress
submitting Form submission in progress
submitted Form successfully submitted
error Validation or submission failed

Validators #

String Validators:

Validator Description Example
Required() Non-null, non-empty Required(message: 'Required')
MinLength(n) Minimum length MinLength(8)
MaxLength(n) Maximum length MaxLength(100)
Email() Valid email format Email()
Url() Valid URL format Url()
Pattern(regex) Matches pattern Pattern(RegExp(r'^[A-Z]+$'))
AlphaNumeric() Letters and numbers only AlphaNumeric()

Number Validators:

Validator Description Example
Min(n) Minimum value Min(0)
Max(n) Maximum value Max(100)
Range(min, max) Within range (inclusive) Range(1, 10)
Integer() Must be integer Integer()
Positive() Must be positive (> 0) Positive()

Date Validators:

Validator Description Example
MinDate(date) On or after date MinDate(DateTime.now())
MaxDate(date) On or before date MaxDate(DateTime(2030))
MinAge(years) Minimum age MinAge(18)
FutureDate() Must be future FutureDate()
PastDate() Must be past PastDate()

Password Validators:

Validator Description Example
HasUppercase() Contains uppercase HasUppercase()
HasLowercase() Contains lowercase HasLowercase()
HasNumber() Contains digit HasNumber()
HasSpecialChar() Contains special char HasSpecialChar()

Comparison Validators:

Validator Description Example
Equals(field) Equals another field Equals('password')
NotEquals(field) Differs from field NotEquals('oldPassword')

Form Fields #

Wrapper components for fifty_ui widgets that integrate with FiftyFormController:

Field Wraps Use Case
FiftyTextFormField FiftyTextField Text input
FiftyDropdownFormField FiftyDropdown Selection from list
FiftyCheckboxFormField FiftyCheckbox Boolean toggle
FiftySwitchFormField FiftySwitch On/off toggle
FiftyRadioFormField FiftyRadioGroup Single selection from options
FiftySliderFormField FiftySlider Numeric range selection
FiftyDateFormField Date picker Date input
FiftyTimeFormField Time picker Time input
FiftyFileFormField File picker File upload

UI Widgets #

Widget Description
FiftyForm Form container with controller binding
FiftySubmitButton Submit button with loading state; optional buttonBuilder
FiftyFormProgress Step progress indicator; optional contentBuilder
FiftyMultiStepForm Multi-step wizard container; optional navigationBuilder
FiftyFormArray Dynamic repeating fields
FiftyFormError Form-level error display
FiftyFieldError Field-level error display
FiftyValidationSummary All errors summary; optional contentBuilder
FiftyFormField Generic field wrapper

Configuration #

FiftyFormController #

Parameter Type Default Description
initialValues Map<String, dynamic> required Initial field values keyed by field name
validators Map<String, List<Validator>> {} Sync validators per field
asyncValidators Map<String, List<AsyncValidator>> {} Async validators per field
onValidationChanged void Function(bool)? null Callback when overall validity changes
validateOnChange bool true Re-validate fields when values change

FiftyMultiStepForm #

Parameter Type Default Description
controller FiftyFormController required Form controller instance
steps List<FormStep> required Step definitions
stepBuilder Widget Function(BuildContext, int, FormStep) required Builder for each step's content
onComplete void Function(Map<String, dynamic>) required Callback on final step completion
onStepChanged void Function(int)? null Callback when active step changes
showProgress bool true Show step progress indicator
validateOnNext bool true Validate current step before advancing
nextLabel String 'NEXT' Label for the next button
previousLabel String 'BACK' Label for the previous button
completeLabel String 'COMPLETE' Label for the final step button
navigationBuilder MultiStepNavigationBuilder? null Optional builder to replace the default back/next/complete buttons

FormStep #

Parameter Type Default Description
title String required Step title for progress indicator
description String? null Optional step description
fields List<String> required Field names to validate for this step
isOptional bool false Can skip if all fields empty
validator String? Function(Map<String, dynamic>)? null Custom step-level validation

DraftManager #

Parameter Type Default Description
controller FiftyFormController required Form controller to manage
key String required Unique storage key for draft
debounce Duration 2 seconds Delay before auto-save triggers
containerName String? 'fifty_forms_drafts' GetStorage container name

FiftyFormArray #

Parameter Type Default Description
controller FiftyFormController required Form controller instance
name String required Base name for array fields
minItems int 0 Minimum items required
maxItems int 10 Maximum items allowed
initialCount int 1 Initial number of items
animate bool true Animate add/remove transitions
itemSpacing double 16 Spacing between items
itemBuilder Widget Function(BuildContext, int, VoidCallback) required Builder for each array item
addButtonBuilder Widget Function(VoidCallback)? null Custom add button builder

Usage Patterns #

Strong Password Validation #

validators: {
  'password': [
    Required(message: 'Password is required'),
    MinLength(8, message: 'At least 8 characters'),
    HasUppercase(message: 'Must contain uppercase letter'),
    HasLowercase(message: 'Must contain lowercase letter'),
    HasNumber(message: 'Must contain a number'),
    HasSpecialChar(message: 'Must contain special character'),
  ],
}

Password Confirmation #

validators: {
  'password': [Required(), MinLength(8)],
  'confirmPassword': [Required(), Equals('password', message: 'Passwords must match')],
}

Custom Validators #

// Synchronous
Custom<String>((value) {
  if (value?.contains('banned') == true) {
    return 'Contains banned word';
  }
  return null;
})

// Asynchronous with debounce
AsyncCustom<String>(
  (value) async {
    final exists = await api.checkUsername(value);
    return exists ? 'Username taken' : null;
  },
  debounce: Duration(milliseconds: 500),
)

Composite Validators #

// And -- all must pass
And([Required(), MinLength(8), HasUppercase()])

// Or -- at least one must pass
Or([Email(), Pattern(phoneRegex)])

Multi-Step Form #

FiftyMultiStepForm(
  controller: controller,
  steps: [
    FormStep(
      title: 'Account',
      description: 'Create your credentials',
      fields: ['email', 'password'],
    ),
    FormStep(
      title: 'Profile',
      fields: ['name', 'bio'],
      isOptional: true,
    ),
    FormStep(
      title: 'Review',
      fields: [],
      validator: (values) {
        if (values['bio']?.isEmpty == true) {
          return 'Consider adding a bio';
        }
        return null;
      },
    ),
  ],
  stepBuilder: (context, index, step) => _buildStep(index),
  onComplete: (values) => api.createUser(values),
  onStepChanged: (step) => print('Now on step $step'),
  showProgress: true,
  validateOnNext: true,
  nextLabel: 'NEXT',
  previousLabel: 'BACK',
  completeLabel: 'COMPLETE',
)

Dynamic Form Arrays #

FiftyFormArray(
  controller: controller,
  name: 'addresses',
  minItems: 1,
  maxItems: 5,
  itemBuilder: (context, index, remove) => Column(
    children: [
      FiftyTextFormField(
        name: 'addresses[$index].street',
        controller: controller,
        label: 'Street',
      ),
      FiftyTextFormField(
        name: 'addresses[$index].city',
        controller: controller,
        label: 'City',
      ),
      IconButton(
        icon: Icon(Icons.delete),
        onPressed: remove,
      ),
    ],
  ),
  addButtonBuilder: (add) => FiftyButton(
    label: 'Add Address',
    onPressed: add,
    variant: FiftyButtonVariant.ghost,
    icon: Icons.add,
  ),
)

Accessing Array Values:

// Get single field
final street = controller.getArrayValue<String>('addresses', 0, 'street');

// Set single field
controller.setArrayValue('addresses', 0, 'city', 'New York');

// Get all items as list of maps
final addresses = controller.getArrayValues('addresses');
// Returns: [{'street': '123 Main', 'city': 'NYC'}, ...]

// Get array length
final count = controller.getArrayLength('addresses');

// Remove item (shifts subsequent indices)
controller.removeArrayItem('addresses', 1);

Draft Persistence #

// Initialize storage once at app start
await DraftManager.initStorage();

// Create draft manager
final draftManager = DraftManager(
  controller: controller,
  key: 'registration_form',
  debounce: Duration(seconds: 2),
);

// Start auto-save
draftManager.start();

// Check for existing draft
if (await draftManager.hasDraft()) {
  final restored = await draftManager.restoreDraft();
  if (restored != null) {
    showSnackBar('Draft restored');
  }
}

// Manual save
await draftManager.saveDraft();

// Clear after successful submission
await controller.submit((values) async {
  await api.save(values);
  await draftManager.clearDraft();
});

// Stop auto-save (keeps draft)
draftManager.stop();

// Cleanup
draftManager.dispose();

FiftySubmitButton #

FiftySubmitButton(
  controller: controller,
  label: 'SUBMIT',
  icon: Icons.send,
  loadingText: 'SAVING...',
  onPressed: () => controller.submit((values) async {
    await api.save(values);
  }),
  disableWhenInvalid: true,
  expanded: true,
  variant: FiftyButtonVariant.primary,
)

Platform Support #

Platform Support Notes
Android Yes
iOS Yes
macOS Yes
Linux Yes
Windows Yes
Web Yes

Fifty Design Language Integration #

This package is part of Fifty Flutter Kit:

  • fifty_tokens - All spacing, radius, and typography values sourced from design tokens
  • fifty_ui - Form field widgets wrap FDL components (FiftyTextField, FiftyDropdown, FiftyCheckbox, etc.)
  • fifty_theme - Consumes theming from the FDL theme system; no custom theme classes defined
  • fifty_storage - Draft persistence layer integrates with the FDL storage abstraction

Version #

Current: 0.2.1


License #

MIT License - see LICENSE for details.

Part of Fifty Flutter Kit.

0
likes
150
points
144
downloads
screenshot

Documentation

API reference

Publisher

verified publisherfifty.dev

Weekly Downloads

Production-ready form building with validation, multi-step wizards, and draft persistence for the Fifty Flutter Kit.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #form #validation #wizard

License

MIT (license)

Dependencies

fifty_storage, fifty_theme, fifty_tokens, fifty_ui, flutter, get_storage

More

Packages that depend on fifty_forms