A declarative, rule-based form validation library for Flutter apps, offering customizable rules and messages, seamless integration with Flutter forms, type safety, and chainable validation.
It provides a simple yet powerful way to define and apply validation logic to your form fields.
✨ Declarative Validation: Define validation rules in a clear, readable way.
🎨 Customizable: Easily tailor rules and error messages to your needs.
🤝 Flutter Integration: Works seamlessly with Flutter's Form
and TextFormField
widgets.
đź”’ Type-Safe: Leverages Dart's type system for safer validation logic.
đź”— Chainable Rules: Combine multiple validation rules effortlessly.
đź“š Comprehensive Built-in Rules: Includes common validation scenarios out-of-the-box (required, email, password, length, numeric range, phone, etc.).
🛠️ Extensible: Create your own custom validation rules by extending the base class.
🔄 Separate Sync/Async APIs: Clearly separated APIs for synchronous and asynchronous validation needs.
🧩 Composable Validators: Combine multiple validators with the CompositeValidator
.
- Installation
- Usage
- Available validation rules
- Creating your own validation rules
- Validation vrchitecture
- Asynchronous validation rules
- Contributing
- License
Add form_shield
to your pubspec.yaml
dependencies:
dependencies:
flutter:
sdk: flutter
form_shield: ^0.4.0
Then, run flutter pub get
.
Import the package:
import 'package:form_shield/form_shield.dart';
Wrap your TextFormField
(or other form fields) within a Form
widget and assign a GlobalKey<FormState>
. Use the Validator
class to attach rules to the validator
property of your fields:
import 'package:flutter/material.dart';
import 'package:form_shield/form_shield.dart';
class MyForm extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: Validator<String>([
RequiredRule(),
EmailRule(),
]),
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: Validator<String>([
RequiredRule(),
PasswordRule(),
]),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Form is valid, proceed
}
},
child: Text('Submit'),
),
],
),
);
}
}
Validator<String>([
RequiredRule(errorMessage: 'Please enter your email address'),
EmailRule(errorMessage: 'Please enter a valid email address'),
])
Validator<String>([
RequiredRule(),
MinLengthRule(8, errorMessage: 'Username must be at least 8 characters'),
MaxLengthRule(20, errorMessage: 'Username cannot exceed 20 characters'),
])
Validator<String>([
RequiredRule(),
CustomRule(
validator: (value) => value != 'admin',
errorMessage: 'Username cannot be "admin"',
),
])
Validator<String>([
DynamicCustomRule(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a value';
}
if (value.contains(' ')) {
return 'No spaces allowed';
}
return null; // Validation passed
},
),
])
Validator<num>([
MinValueRule(18, errorMessage: 'You must be at least 18 years old'),
MaxValueRule(120, errorMessage: 'Please enter a valid age'),
])
// General phone validation
Validator<String>([
RequiredRule(),
PhoneRule(),
])
// Country-specific phone validation
Validator<String>([
RequiredRule(),
CountryPhoneRule(countryCode: 'CM'),
])
Validator<String>([
RequiredRule(),
PasswordRule(
options: PasswordOptions(
minLength: 10,
requireUppercase: true,
requireLowercase: true,
requireDigit: true,
requireSpecialChar: true,
),
errorMessage: 'Password does not meet security requirements',
),
])
final passwordController = TextEditingController();
// Password field
TextFormField(
controller: passwordController,
validator: Validator<String>([
RequiredRule(),
PasswordRule(),
]),
)
// Confirm password field
TextFormField(
validator: Validator<String>([
RequiredRule(),
PasswordMatchRule(
passwordGetter: () => passwordController.text,
errorMessage: 'Passwords do not match',
),
]),
)
RequiredRule
- Validates that a value is not null or emptyEmailRule
- Validates that a string is a valid email addressPasswordRule
- Validates that a string meets password requirementsPasswordMatchRule
- Validates that a string matches another stringLengthRule
- Validates that a string's length is within specified boundsMinLengthRule
- Validates that a string's length is at least a specified minimumMaxLengthRule
- Validates that a string's length is at most a specified maximumValueRule
- Validates that a numeric value is within specified boundsMinValueRule
- Validates that a numeric value is at least a specified minimumMaxValueRule
- Validates that a numeric value is at most a specified maximumPhoneRule
- Validates that a string is a valid phone numberCountryPhoneRule
- Validates that a string is a valid phone number for a specific countryUrlRule
- Validates that a string is a valid URLIPAddressRule
- Validates that a string is a valid IPv4 or IPv6 addressCreditCardRule
- Validates that a string is a valid credit card numberDateRangeRule
- Validates that a date is within a specified rangeCustomRule
- A validation rule that uses a custom function to validate valuesDynamicCustomRule
- A validation rule that uses a custom function to validate values and return a dynamic error message
You can create your own validation rules by extending the ValidationRule
class:
class NoSpacesRule extends ValidationRule<String> {
const NoSpacesRule({
String errorMessage = 'No spaces allowed',
}) : super(errorMessage: errorMessage);
@override
ValidationResult validate(String? value) {
if (value == null || value.isEmpty) {
return const ValidationResult.success();
}
if (value.contains(' ')) {
return ValidationResult.error(errorMessage);
}
return const ValidationResult.success();
}
}
Then use it like any other validation rule:
Validator<String>([
RequiredRule(),
NoSpacesRule(),
])
Form Shield offers three distinct validator classes to handle different validation scenarios:
Validator
handles synchronous validation with immediate results:
// Create a validator with synchronous rules
final validator = Validator<String>([
RequiredRule(),
EmailRule(),
]);
// Use it directly as a FormField validator
TextFormField(
validator: validator,
)
AsyncValidator
is specifically for asynchronous validation needs:
// Create an async validator with async rules
final asyncValidator = AsyncValidator<String>([
UsernameAvailabilityRule(
checkAvailability: (username) async {
// API call or database check
return await userRepository.isUsernameAvailable(username);
},
),
], debounceDuration: Duration(milliseconds: 500));
// Don't forget to dispose
@override
void dispose() {
asyncValidator.dispose();
super.dispose();
}
CompositeValidator
combines both synchronous and asynchronous validators:
// Create sync and async validators
final syncValidator = Validator<String>([
RequiredRule(),
MinLengthRule(3),
MaxLengthRule(20),
]);
final asyncValidator = AsyncValidator<String>([
UsernameAvailabilityRule(
checkAvailability: _checkUsernameAvailability,
),
]);
// Compose them together
final compositeValidator = CompositeValidator<String>(
syncValidators: [syncValidator],
asyncValidators: [asyncValidator],
);
// Use in your form
TextFormField(
validator: compositeValidator,
// ...
)
// Clean up resources
@override
void dispose() {
compositeValidator.dispose();
super.dispose();
}
Form Shield supports asynchronous validation for scenarios where validation requires network requests or other async operations (like checking username availability or email uniqueness).
You can create async validation rules by extending the specialized AsyncValidationRule
class:
class UsernameAvailabilityRule extends AsyncValidationRule<String> {
final Future<bool> Function(String username) _checkAvailability;
const UsernameAvailabilityRule({
required Future<bool> Function(String username) checkAvailability,
super.errorMessage = 'This username is already taken',
}) : _checkAvailability = checkAvailability;
@override
ValidationResult validate(String? value) {
// Basic sync validation for null/empty check
if (value == null || value.isEmpty) {
return ValidationResult.error('Username cannot be empty');
}
return const ValidationResult.success();
}
@override
Future<ValidationResult> validateAsync(String? value) async {
// Run sync validation first
final syncResult = validate(value);
if (!syncResult.isValid) {
return syncResult;
}
try {
// Perform the async validation
final isAvailable = await _checkAvailability(value!);
if (isAvailable) {
return const ValidationResult.success();
} else {
return ValidationResult.error(errorMessage);
}
} catch (e) {
return ValidationResult.error('Error checking username availability: $e');
}
}
}
When using async validation, use the AsyncValidator
or CompositeValidator
class:
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
late final AsyncValidator<String> _asyncValidator;
late final CompositeValidator<String> _compositeValidator;
@override
void initState() {
super.initState();
_asyncValidator = AsyncValidator<String>([
UsernameAvailabilityRule(
checkAvailability: _checkUsernameAvailability,
),
], debounceDuration: Duration(milliseconds: 500));
_compositeValidator = CompositeValidator<String>(
syncValidators: [
Validator<String>([
RequiredRule(),
LengthRule(minLength: 3, maxLength: 20),
]),
],
asyncValidators: [_asyncValidator],
);
}
@override
void dispose() {
_usernameController.dispose();
_compositeValidator.dispose(); // This will handle disposing the async validator
super.dispose();
}
Future<bool> _checkUsernameAvailability(String username) async {
// Simulate API call with delay
await Future.delayed(const Duration(seconds: 1));
final takenUsernames = ['admin', 'user', 'test'];
return !takenUsernames.contains(username.toLowerCase());
}
void _submitForm() {
if (_formKey.currentState!.validate() &&
!_compositeValidator.isValidating &&
_compositeValidator.isValid) {
// All validations passed, proceed with form submission
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: InputDecoration(labelText: 'Username'),
validator: _compositeValidator,
),
// Show async validation state
ListenableBuilder(
listenable: _asyncValidator.asyncState,
builder: (context, _) {
if (_compositeValidator.isValidating) {
return Text('Checking username availability...');
} else if (!_compositeValidator.isValid && _asyncValidator.errorMessage != null) {
return Text(
_asyncValidator.errorMessage!,
style: TextStyle(color: Colors.red),
);
} else if (_compositeValidator.isValid) {
return Text(
'Username is available',
style: TextStyle(color: Colors.green),
);
}
return SizedBox.shrink();
},
),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
);
}
}
AsyncValidator includes built-in debouncing to prevent excessive API calls during typing. You can customize the debounce duration:
AsyncValidator<String>([
UsernameAvailabilityRule(checkAvailability: _checkUsername),
], debounceDuration: Duration(milliseconds: 800)) // Custom debounce time
You can manually trigger async validation using the validateAsync
method:
Future<void> _checkUsername() async {
final isValid = await _asyncValidator.validateAsync(
_usernameController.text,
debounceDuration: Duration.zero, // Optional: skip debouncing
);
if (isValid) {
// Username is valid and available
}
}
You can use the ListenableBuilder
widget to listen to the async validation state and show the error message when it becomes available. Here's an example:
```dart
ListenableBuilder(
listenable: _asyncValidator.asyncState,
builder: (context, _) {
if (_compositeValidator.isValidating) {
return Text('Checking username availability...');
} else if (!_compositeValidator.isValid && _asyncValidator.errorMessage != null) {
return Text(
_asyncValidator.errorMessage!,
style: TextStyle(color: Colors.red),
);
} else if (_compositeValidator.isValid) {
return Text(
'Username is available',
style: TextStyle(color: Colors.green),
);
}
return SizedBox.shrink();
},
)
This will show the error message when the async validation fails, and a success message when it passes.
For live validation feedback as the user types, make sure to set autovalidateMode
on your Form:
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.always, // Enable live validation
child: Column(
// Form fields...
),
)
This ensures validation runs automatically whenever input changes, providing immediate feedback.
Contributions are welcome! Please feel free to submit issues, pull requests, or suggest improvements.
This project is licensed under the MIT License - see the LICENSE file for details.