A comprehensive Flutter package for form handling built on Riverpod, providing robust validation, state management, and type-safe form field handling with minimal boilerplate.
- 🎯 Type-safe form fields with built-in validation
- 🔄 Reactive state management using Riverpod
- ✅ Extensive validators for text, numbers, dates, dropdowns, multi-select, images, and more
- 🎨 Clean architecture following Domain-Driven Design principles
- 🚀 Zero boilerplate with code generation using Freezed
- 📱 Flutter-first design with seamless UI integration
- 🔐 Immutable state for predictable form behavior
- 💡 Smart error handling with lazy validation
Add this to your package's pubspec.yaml file:
dependencies:
form_handling_flutter: ^latest_version
flutter_riverpod: ^2.0.0
freezed_annotation: ^2.0.0
dartz: ^0.10.1 # For functional programming (Either, Option)
dev_dependencies:
build_runner: ^2.0.0
freezed: ^2.0.0Then run:
flutter pub getimport 'package:form_handling_flutter/form_handling.dart';
// Create form field objects using the generate factory
final emailField = StringFieldObject.generate(
value: null,
validator: TextValidator.email(), // Built-in email validator
);
final passwordField = StringFieldObject.generate(
value: null,
validator: TextValidator.password(), // Built-in password validator
);
// Or create custom validators
final nameField = StringFieldObject.generate(
value: null,
validator: TextValidator(
minLength: 2,
maxLength: 50,
isRequired: true,
),
);import 'package:dartz/dartz.dart';
import 'package:form_handling_flutter/form_handling.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
part 'sign_in_notifier.freezed.dart';
@freezed
class SignInFormFields with _$SignInFormFields, FormFieldsMixin {
const SignInFormFields._();
const factory SignInFormFields({
required StringFieldObject email,
required StringFieldObject password,
}) = _SignInFormFields;
factory SignInFormFields.generate() {
return SignInFormFields(
email: StringFieldObject.generate(
value: null,
validator: TextValidator.email(),
),
password: StringFieldObject.generate(
value: null,
validator: TextValidator.password(),
),
);
}
@override
List<FormFieldObject> get fieldsList => [email, password];
}
@freezed
class SignInFailure with _$SignInFailure {
const factory SignInFailure.serverError() = _ServerError;
const factory SignInFailure.invalidCredentials() = _InvalidCredentials;
}
class SignInNotifier extends FormNotifier<SignInFormFields, SignInFailure> {
SignInNotifier() : super(() => SignInFormFields.generate());
Future<void> signIn() async {
if (!validateFormAndSave()) {
return;
}
state = CustomFormState.inProgress(fields: fields);
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
// Example: Always succeed for demo
final failureOrUnit = right(unit);
state = failureOrUnit.fold(
(failure) => CustomFormState.failure(failure, fields: fields),
(_) => CustomFormState.success(fields: fields),
);
}
}
final signInNotifierProvider = StateNotifierProvider<SignInNotifier, CustomFormState<SignInFormFields, SignInFailure>>(
(ref) => SignInNotifier(),
);import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_handling_flutter/form_handling.dart';
class SignInForm extends ConsumerWidget {
const SignInForm({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(signInNotifierProvider);
final notifier = ref.watch(signInNotifierProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text('Sign In'),
),
body: Form(
key: notifier.formKey,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
errorText: state.fields.email.showError
? state.fields.email.errorMessage
: null,
),
keyboardType: TextInputType.emailAddress,
onChanged: state.fields.email.setValue,
initialValue: state.fields.email.initialValue,
),
const SizedBox(height: 20),
TextFormField(
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.vpn_key),
errorText: state.fields.password.showError
? state.fields.password.errorMessage
: null,
),
obscureText: true,
onChanged: state.fields.password.setValue,
initialValue: state.fields.password.initialValue,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: state.maybeWhen(
inProgress: (fields) => null,
orElse: () => notifier.signIn,
),
child: state.maybeWhen(
inProgress: (fields) => CircularProgressIndicator(),
orElse: () => Text('Sign In'),
),
),
if (state is _Failure)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
'Sign in failed',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
),
),
),
);
}
}For text input with comprehensive validation options:
final nameField = StringFieldObject.generate(
value: null,
validator: TextValidator(
minLength: 2,
maxLength: 50,
isRequired: true,
regex: r'^[a-zA-Z\s]+$',
customError: 'Please enter a valid name',
),
);
// Built-in validators
final emailField = StringFieldObject.generate(
value: null,
validator: TextValidator.email(),
);
final passwordField = StringFieldObject.generate(
value: null,
validator: TextValidator.password(),
);For numeric inputs:
final ageField = IntFieldObject.generate(
value: null,
validator: IntValidator(
min: 18,
max: 120,
isRequired: true,
),
);
final priceField = DoubleFieldObject.generate(
value: null,
validator: DoubleValidator(
min: 0.01,
max: 10000.0,
isRequired: true,
),
);For date and time inputs:
final birthDateField = DateTimeFieldObject.generate(
value: null,
validator: DateValidator(
minDate: DateTime(1900),
maxDate: DateTime.now(),
isRequired: true,
),
);For checkboxes and toggles:
final termsAcceptedField = BoolFieldObject.generate(
value: false,
validator: BoolValidator(
mustBeTrue: true,
customError: 'You must accept the terms',
),
);For single selection:
final countryField = DropdownFieldObject<String>.generate(
value: null,
validator: DropdownValidator(
isRequired: true,
),
);For multiple selections:
final categoriesField = MultiSelectFieldObject<String>.generate(
value: [],
validator: MultiSelectValidator(
minSelections: 1,
maxSelections: 5,
isRequired: true,
),
);For image uploads with type validation:
final avatarField = ImageFieldObject.generate(
value: null,
validator: ImageValidator(
allowedTypes: [ImageType.jpg, ImageType.png],
maxSizeInMB: 5,
isRequired: true,
),
);For custom validation logic:
final customField = CustomFieldObject<MyCustomType>.generate(
value: null,
validator: CustomValidator<MyCustomType>(
validate: (value) {
if (value == null) return 'Value is required';
// Custom validation logic
return null;
},
),
);The CustomFormState class represents the overall form status and contains the fields:
state.when(
initial: (fields) => print('Form is ready'),
inProgress: (fields) => print('Form is being submitted'),
success: (fields) => print('Success!'),
failure: (failure, fields) => print('Error: $failure'),
);
// Access fields from state
final email = state.fields.email;
final password = state.fields.password;
// Check state type
if (state is CustomFormStateInProgress) {
// Show loading indicator
}Create custom validators by extending the base validator classes:
class PhoneNumberValidator extends TextValidator {
PhoneNumberValidator() : super(
regex: r'^\+?[1-9]\d{1,14}$',
customError: 'Please enter a valid phone number',
isRequired: true,
);
}Reset all fields to their initial state:
notifier.reset();Fields can have dynamic validation based on other fields:
void updatePasswordValidation(bool requireStrongPassword) {
final newValidator = TextValidator(
minLength: requireStrongPassword ? 12 : 8,
isRequired: true,
regex: requireStrongPassword
? r'^(?=.*[A-Z])(?=.*[!@#$%^&*]).*$'
: null,
);
final updatedPassword = StringFieldObject.generate(
value: state.fields.password.value,
validator: newValidator,
);
state = state.copyWith(
fields: state.fields.copyWith(password: updatedPassword),
);
}- Always use Freezed for your form state classes to ensure immutability
- Validate on submit rather than on every keystroke for better UX
- Use
showErrorgetter to display errors only after validation attempts - Handle all form states (initial, in-progress, success, failure) in your UI
- Dispose form notifiers properly to avoid memory leaks
- Keep validators reusable by creating custom validator classes
Check out the /example folder for a complete implementation of a sign-in form with:
- Email validation
- Password validation
- Error handling
- Loading states
- Success/failure feedback
To run the example:
cd example
flutter runContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If you find this package helpful, please give it a ⭐ on GitHub!
For issues and feature requests, please create an issue.