# Forms Form validation, FormField patterns, input formatting, and reusable form components for Flutter. ## Basic Form Setup ```dart class LoginForm extends StatefulWidget { const LoginForm({super.key}); @override State createState() => _LoginFormState(); } class _LoginFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { await authService.login( email: _emailController.text.trim(), password: _passwordController.text, ); } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email_outlined), ), keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autocorrect: false, validator: Validators.email, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', prefixIcon: Icon(Icons.lock_outlined), ), obscureText: true, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(), validator: Validators.password, ), const SizedBox(height: 24), ElevatedButton( onPressed: _isLoading ? null : _submit, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Login'), ), ], ), ); } } ``` ## Validators ```dart class Validators { static String? required(String? value) { if (value == null || value.trim().isEmpty) { return 'This field is required'; } return null; } static String? email(String? value) { if (value == null || value.trim().isEmpty) { return 'Email is required'; } final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!regex.hasMatch(value.trim())) { return 'Enter a valid email address'; } return null; } static String? password(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; } static String? strongPassword(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } if (!RegExp(r'[A-Z]').hasMatch(value)) { return 'Password must contain an uppercase letter'; } if (!RegExp(r'[a-z]').hasMatch(value)) { return 'Password must contain a lowercase letter'; } if (!RegExp(r'[0-9]').hasMatch(value)) { return 'Password must contain a number'; } return null; } static String? phone(String? value) { if (value == null || value.trim().isEmpty) { return 'Phone number is required'; } final digits = value.replaceAll(RegExp(r'\D'), ''); if (digits.length < 10 || digits.length > 15) { return 'Enter a valid phone number'; } return null; } static String? minLength(int min) { return (String? value) { if (value == null || value.length < min) { return 'Must be at least $min characters'; } return null; }; } static String? maxLength(int max) { return (String? value) { if (value != null && value.length > max) { return 'Must be at most $max characters'; } return null; }; } static String? Function(String?) combine(List validators) { return (String? value) { for (final validator in validators) { final error = validator(value); if (error != null) return error; } return null; }; } static String? match(String pattern, String message) { return (String? value) { if (value != null && !RegExp(pattern).hasMatch(value)) { return message; } return null; }; } static String? confirmPassword(TextEditingController passwordController) { return (String? value) { if (value != passwordController.text) { return 'Passwords do not match'; } return null; }; } } ``` ## Input Formatters ```dart import 'package:flutter/services.dart'; class PhoneInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); final buffer = StringBuffer(); for (int i = 0; i < digits.length && i < 10; i++) { if (i == 3 || i == 6) buffer.write('-'); buffer.write(digits[i]); } return TextEditingValue( text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length), ); } } class CreditCardFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); final buffer = StringBuffer(); for (int i = 0; i < digits.length && i < 16; i++) { if (i > 0 && i % 4 == 0) buffer.write(' '); buffer.write(digits[i]); } return TextEditingValue( text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length), ); } } class CurrencyInputFormatter extends TextInputFormatter { final int decimalPlaces; CurrencyInputFormatter({this.decimalPlaces = 2}); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (newValue.text.isEmpty) return newValue; final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), ''); if (digits.isEmpty) return const TextEditingValue(text: ''); final value = int.parse(digits) / 100; final formatted = value.toStringAsFixed(decimalPlaces); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } } class UpperCaseFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { return newValue.copyWith(text: newValue.text.toUpperCase()); } } ``` ### Using Formatters ```dart TextFormField( decoration: const InputDecoration(labelText: 'Phone'), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, PhoneInputFormatter(), ], ) TextFormField( decoration: const InputDecoration(labelText: 'Amount'), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[\d.]')), CurrencyInputFormatter(), ], ) ``` ## Custom FormFields ### Dropdown FormField ```dart class DropdownFormField extends FormField { DropdownFormField({ super.key, required List> items, super.initialValue, super.validator, super.onSaved, String? labelText, String? hintText, ValueChanged? onChanged, }) : super( builder: (state) { return InputDecorator( decoration: InputDecoration( labelText: labelText, errorText: state.errorText, ), child: DropdownButtonHideUnderline( child: DropdownButton( value: state.value, hint: hintText != null ? Text(hintText) : null, isExpanded: true, items: items, onChanged: (value) { state.didChange(value); onChanged?.call(value); }, ), ), ); }, ); } ``` ### Checkbox FormField ```dart class CheckboxFormField extends FormField { CheckboxFormField({ super.key, required Widget label, super.initialValue = false, super.validator, super.onSaved, }) : super( builder: (state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Checkbox( value: state.value ?? false, onChanged: state.didChange, ), Expanded(child: GestureDetector( onTap: () => state.didChange(!(state.value ?? false)), child: label, )), ], ), if (state.hasError) Padding( padding: const EdgeInsets.only(left: 12, top: 4), child: Text( state.errorText!, style: TextStyle( color: Theme.of(state.context).colorScheme.error, fontSize: 12, ), ), ), ], ); }, ); } ``` ### Date Picker FormField ```dart class DatePickerFormField extends FormField { DatePickerFormField({ super.key, super.initialValue, super.validator, super.onSaved, String? labelText, DateTime? firstDate, DateTime? lastDate, }) : super( builder: (state) { return GestureDetector( onTap: () async { final picked = await showDatePicker( context: state.context, initialDate: state.value ?? DateTime.now(), firstDate: firstDate ?? DateTime(1900), lastDate: lastDate ?? DateTime(2100), ); if (picked != null) { state.didChange(picked); } }, child: InputDecorator( decoration: InputDecoration( labelText: labelText, errorText: state.errorText, suffixIcon: const Icon(Icons.calendar_today), ), child: Text( state.value != null ? DateFormat.yMMMd().format(state.value!) : 'Select date', ), ), ); }, ); } ``` ## Form with Hooks ```dart import 'package:flutter_hooks/flutter_hooks.dart'; class HookLoginForm extends HookWidget { const HookLoginForm({super.key}); @override Widget build(BuildContext context) { final formKey = useMemoized(GlobalKey.new); final emailController = useTextEditingController(); final passwordController = useTextEditingController(); final emailFocus = useFocusNode(); final passwordFocus = useFocusNode(); final isLoading = useState(false); Future submit() async { if (!formKey.currentState!.validate()) return; isLoading.value = true; try { await authService.login( email: emailController.text.trim(), password: passwordController.text, ); } finally { isLoading.value = false; } } return Form( key: formKey, child: Column( children: [ TextFormField( controller: emailController, focusNode: emailFocus, decoration: const InputDecoration(labelText: 'Email'), textInputAction: TextInputAction.next, onFieldSubmitted: (_) => passwordFocus.requestFocus(), validator: Validators.email, ), const SizedBox(height: 16), TextFormField( controller: passwordController, focusNode: passwordFocus, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, onFieldSubmitted: (_) => submit(), validator: Validators.password, ), const SizedBox(height: 24), ElevatedButton( onPressed: isLoading.value ? null : submit, child: isLoading.value ? const CircularProgressIndicator() : const Text('Login'), ), ], ), ); } } ``` ## Server-Side Validation ```dart class ServerValidationForm extends StatefulWidget { const ServerValidationForm({super.key}); @override State createState() => _ServerValidationFormState(); } class _ServerValidationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); Map> _serverErrors = {}; String? _emailValidator(String? value) { final clientError = Validators.email(value); if (clientError != null) return clientError; final serverError = _serverErrors['email']; if (serverError != null && serverError.isNotEmpty) { return serverError.first; } return null; } Future _submit() async { setState(() => _serverErrors = {}); if (!_formKey.currentState!.validate()) return; try { await api.register(email: _emailController.text); } on ValidationException catch (e) { setState(() => _serverErrors = e.errors); _formKey.currentState!.validate(); } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), validator: _emailValidator, onChanged: (_) { if (_serverErrors.containsKey('email')) { setState(() => _serverErrors.remove('email')); } }, ), ElevatedButton( onPressed: _submit, child: const Text('Register'), ), ], ), ); } } ``` ## Auto-Save Form ```dart class AutoSaveForm extends StatefulWidget { const AutoSaveForm({super.key}); @override State createState() => _AutoSaveFormState(); } class _AutoSaveFormState extends State { final _formKey = GlobalKey(); Timer? _debounce; bool _hasChanges = false; void _onChanged() { setState(() => _hasChanges = true); _debounce?.cancel(); _debounce = Timer(const Duration(seconds: 2), _autoSave); } Future _autoSave() async { if (!_hasChanges) return; if (!_formKey.currentState!.validate()) return; _formKey.currentState!.save(); await saveToServer(); setState(() => _hasChanges = false); } @override void dispose() { _debounce?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Form( key: _formKey, onChanged: _onChanged, child: Column( children: [ if (_hasChanges) const Text('Saving...', style: TextStyle(color: Colors.grey)), TextFormField( decoration: const InputDecoration(labelText: 'Title'), onSaved: (value) => saveField('title', value), ), TextFormField( decoration: const InputDecoration(labelText: 'Description'), maxLines: 3, onSaved: (value) => saveField('description', value), ), ], ), ); } } ``` ## Common Keyboard Types | Type | Usage | |------|-------| | `TextInputType.text` | General text | | `TextInputType.emailAddress` | Email with @ keyboard | | `TextInputType.phone` | Phone number pad | | `TextInputType.number` | Numeric keyboard | | `TextInputType.numberWithOptions(decimal: true)` | Numbers with decimal | | `TextInputType.multiline` | Multi-line text | | `TextInputType.url` | URL with shortcuts | ## Form Checklist | Item | Implementation | |------|----------------| | GlobalKey | `GlobalKey()` for form | | Dispose controllers | Clean up in `dispose()` | | Validation | Client + server-side | | Input formatters | Phone, currency, etc. | | Keyboard types | Match input type | | Text actions | `textInputAction` for flow | | Loading state | Disable during submission | | Error display | Show below fields | --- *Flutter and Material Design are trademarks of Google LLC.*