A comprehensive form state management hook with validation, submission handling, and field management.
import 'package:flutter_use/flutter_use.dart';
class LoginForm extends HookWidget {
@override
Widget build(BuildContext context) {
final emailField = useField<String>(
initialValue: '',
validators: [
Validators.required(),
Validators.email(),
],
);
final passwordField = useField<String>(
initialValue: '',
validators: [
Validators.required(),
Validators.minLength(8),
],
);
final form = useForm({
'email': emailField,
'password': passwordField,
});
return Form(
child: Column(
children: [
TextFormField(
controller: emailField.controller,
focusNode: emailField.focusNode,
decoration: InputDecoration(
labelText: 'Email',
errorText: emailField.showError ? emailField.error : null,
),
),
const SizedBox(height: 16),
TextFormField(
controller: passwordField.controller,
focusNode: passwordField.focusNode,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: passwordField.showError ? passwordField.error : null,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: form.isValid && !form.isSubmitting
? () => form.submit((values) async {
await authService.login(
values['email'],
values['password'],
);
Navigator.pushReplacementNamed(context, '/home');
})
: null,
child: form.isSubmitting
? const CircularProgressIndicator()
: const Text('Login'),
),
if (form.submitError != null)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
form.submitError!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
}class RegistrationForm extends HookWidget {
@override
Widget build(BuildContext context) {
final form = useForm({
'username': useField<String>(
validators: [
Validators.required('Username is required'),
Validators.minLength(3, 'At least 3 characters'),
Validators.maxLength(20, 'At most 20 characters'),
Validators.pattern(
RegExp(r'^[a-zA-Z0-9_]+$'),
'Only letters, numbers, and underscores',
),
],
),
'email': useField<String>(
validators: [
Validators.required(),
Validators.email('Invalid email format'),
],
),
'age': useField<int>(
validators: [
Validators.required(),
Validators.range(18, 120, 'Must be 18 or older'),
],
),
'password': useField<String>(
validators: [
Validators.required(),
Validators.minLength(8),
// Custom validator
(value) {
if (value != null && !value.contains(RegExp(r'[0-9]'))) {
return 'Password must contain at least one number';
}
return null;
},
],
),
});
// Form UI...
}
}FieldState<T> useField<T>({
T? initialValue,
List<FieldValidator<T>> validators = const [],
bool validateOnChange = false,
})Parameters:
initialValue: Initial field valuevalidators: List of validation functionsvalidateOnChange: Validate on every change (after touched)
Returns: FieldState<T> with:
value: Current valueerror: Validation error messagetouched: Whether field has been focusedsetValue(T?): Update valuesetError(String?): Set error manuallysetTouched(bool): Mark as touchedvalidate(): Run validationreset(): Reset to initial statecontroller: TextEditingController (for String fields)focusNode: FocusNode for the fieldisValid: Whether field is validshowError: Whether to display error (touched && error)
FormState useForm(Map<String, FieldState> fields)Parameters:
fields: Map of field names to field states
Returns: FormState with:
fields: Map of all fieldsisValid: Whether all fields are validisDirty: Whether any field has changedisSubmitting: Whether form is being submittedsubmitError: Error from submissionvalues: Current form valueserrors: Current field errorsvalidate(): Validate all fieldssubmit(Function): Submit with handlerreset(): Reset all fields
Built-in validators:
Validators.required<T>([message]): Non-empty validationValidators.email([message]): Email format validationValidators.minLength(length, [message]): Minimum lengthValidators.maxLength(length, [message]): Maximum lengthValidators.pattern(regex, [message]): Pattern matchingValidators.range(min, max, [message]): Number range
- Comprehensive field state management
- Built-in and custom validators
- Automatic TextEditingController for text fields
- Focus management
- Touch state tracking
- Form-level validation
- Async submission handling
- Error state management
- Field reset functionality
- Compose multiple validators
-
User Registration
final form = useForm({ 'email': useField(validators: [Validators.required(), Validators.email()]), 'password': useField(validators: [Validators.required(), Validators.minLength(8)]), 'confirmPassword': useField(validators: [ Validators.required(), (value) => value != passwordField.value ? 'Passwords must match' : null, ]), });
-
Profile Settings
final form = useForm({ 'displayName': useField(initialValue: user.name), 'bio': useField(initialValue: user.bio, validators: [Validators.maxLength(200)]), 'website': useField(validators: [urlValidator]), });
-
Multi-step Forms
final step1 = useForm({...}); final step2 = useForm({...}); final currentStep = useState(0);
class CompleteFormExample extends HookWidget {
@override
Widget build(BuildContext context) {
// Custom async validator
final checkUsernameAvailable = useCallback((String? username) async {
if (username == null || username.isEmpty) return null;
final isAvailable = await api.checkUsername(username);
return isAvailable ? null : 'Username already taken';
}, []);
final usernameField = useField<String>(
validators: [
Validators.required(),
Validators.pattern(RegExp(r'^[a-zA-Z0-9_]+$')),
],
);
final emailField = useField<String>(
validators: [Validators.required(), Validators.email()],
validateOnChange: true,
);
final passwordField = useField<String>(
validators: [
Validators.required(),
Validators.minLength(8),
(value) {
if (value == null) return null;
final hasUpper = value.contains(RegExp(r'[A-Z]'));
final hasLower = value.contains(RegExp(r'[a-z]'));
final hasDigit = value.contains(RegExp(r'[0-9]'));
final hasSpecial = value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
if (!hasUpper || !hasLower || !hasDigit || !hasSpecial) {
return 'Password must contain uppercase, lowercase, number, and special character';
}
return null;
},
],
);
final form = useForm({
'username': usernameField,
'email': emailField,
'password': passwordField,
});
// Async username validation
useEffect(() {
Timer? timer;
if (usernameField.value?.isNotEmpty ?? false) {
timer = Timer(const Duration(milliseconds: 500), () async {
final error = await checkUsernameAvailable(usernameField.value);
usernameField.setError(error);
});
}
return timer?.cancel;
}, [usernameField.value]);
return Scaffold(
appBar: AppBar(title: const Text('Sign Up')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: usernameField.controller,
focusNode: usernameField.focusNode,
decoration: InputDecoration(
labelText: 'Username',
errorText: usernameField.showError ? usernameField.error : null,
suffixIcon: usernameField.value?.isNotEmpty ?? false
? usernameField.error == null
? const Icon(Icons.check, color: Colors.green)
: const Icon(Icons.error, color: Colors.red)
: null,
),
),
const SizedBox(height: 16),
TextFormField(
controller: emailField.controller,
focusNode: emailField.focusNode,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
errorText: emailField.showError ? emailField.error : null,
),
),
const SizedBox(height: 16),
TextFormField(
controller: passwordField.controller,
focusNode: passwordField.focusNode,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: passwordField.showError ? passwordField.error : null,
helperText: 'Min 8 chars with upper, lower, number, special',
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: form.reset,
child: const Text('Reset'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: form.isValid && !form.isSubmitting
? () => form.submit((values) async {
await api.createAccount(values);
if (context.mounted) {
Navigator.pushReplacementNamed(context, '/welcome');
}
})
: null,
child: form.isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign Up'),
),
),
],
),
],
),
),
);
}
}