Flutter Form Widgets: Handling User Input
Form widgets are essential for handling user input and data collection in Flutter applications. This comprehensive guide covers everything from basic form widgets to advanced form handling patterns and best practices.
1. Basic Form Widgets
TextFormField
The TextFormField
is one of the most commonly used form widgets, providing text input with built-in validation capabilities.
class TextFormFieldExample extends StatefulWidget { @override _TextFormFieldExampleState createState() => _TextFormFieldExampleState(); } class _TextFormFieldExampleState extends State<TextFormFieldExample> { final _formKey = GlobalKey<FormState>(); final _textController = TextEditingController(); @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _textController, decoration: InputDecoration( labelText: 'Enter text', hintText: 'Type something...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.text_fields), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter some text'; } if (value.length < 3) { return 'Text must be at least 3 characters long'; } return null; }, ), SizedBox(height: 16), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Handle form submission print('Text: ${_textController.text}'); } }, child: Text('Submit'), ), ], ), ); } }
CheckboxListTile
CheckboxListTile
provides a checkbox with a label, perfect for terms and conditions or multiple selection scenarios.
class CheckboxListTileExample extends StatefulWidget { @override _CheckboxListTileExampleState createState() => _CheckboxListTileExampleState(); } class _CheckboxListTileExampleState extends State<CheckboxListTileExample> { bool _isChecked = false; @override Widget build(BuildContext context) { return CheckboxListTile( title: Text('Accept Terms and Conditions'), subtitle: Text('Please read and accept our terms and conditions'), value: _isChecked, onChanged: (value) { setState(() { _isChecked = value!; }); }, secondary: Icon(Icons.description), controlAffinity: ListTileControlAffinity.leading, ); } }
2. Advanced Form Widgets
RadioListTile
RadioListTile
is ideal for single-selection scenarios, such as choosing a payment method or delivery option.
class RadioListTileExample extends StatefulWidget { @override _RadioListTileExampleState createState() => _RadioListTileExampleState(); } class _RadioListTileExampleState extends State<RadioListTileExample> { String _selectedOption = 'Option 1'; @override Widget build(BuildContext context) { return Column( children: [ RadioListTile( title: Text('Option 1'), subtitle: Text('Basic plan'), value: 'Option 1', groupValue: _selectedOption, onChanged: (value) { setState(() { _selectedOption = value.toString(); }); }, secondary: Icon(Icons.star_border), ), RadioListTile( title: Text('Option 2'), subtitle: Text('Premium plan'), value: 'Option 2', groupValue: _selectedOption, onChanged: (value) { setState(() { _selectedOption = value.toString(); }); }, secondary: Icon(Icons.star), ), RadioListTile( title: Text('Option 3'), subtitle: Text('Enterprise plan'), value: 'Option 3', groupValue: _selectedOption, onChanged: (value) { setState(() { _selectedOption = value.toString(); }); }, secondary: Icon(Icons.star_half), ), ], ); } }
Slider
The Slider
widget is perfect for selecting a value within a range, such as volume control or price range selection.
class SliderExample extends StatefulWidget { @override _SliderExampleState createState() => _SliderExampleState(); } class _SliderExampleState extends State<SliderExample> { double _sliderValue = 0.0; @override Widget build(BuildContext context) { return Column( children: [ Slider( value: _sliderValue, min: 0.0, max: 100.0, divisions: 10, label: _sliderValue.round().toString(), onChanged: (value) { setState(() { _sliderValue = value; }); }, activeColor: Theme.of(context).primaryColor, inactiveColor: Theme.of(context).primaryColor.withOpacity(0.3), ), Text( 'Value: ${_sliderValue.round()}', style: Theme.of(context).textTheme.titleMedium, ), ], ); } }
3. Custom Form Widgets
Custom Form Field
Creating custom form fields helps maintain consistency across your application and reduces code duplication.
class CustomFormField extends StatelessWidget { final String label; final String? Function(String?)? validator; final TextEditingController controller; final bool obscureText; final IconData? prefixIcon; final String? hintText; const CustomFormField({ required this.label, required this.validator, required this.controller, this.obscureText = false, this.prefixIcon, this.hintText, }); @override Widget build(BuildContext context) { return TextFormField( controller: controller, obscureText: obscureText, decoration: InputDecoration( labelText: label, hintText: hintText, border: OutlineInputBorder(), errorBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.red), ), prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null, ), validator: validator, ); } }
Custom Form with Validation
A complete form example with multiple fields and validation.
class CustomForm extends StatefulWidget { @override _CustomFormState createState() => _CustomFormState(); } class _CustomFormState extends State<CustomForm> { final _formKey = GlobalKey<FormState>(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _emailController = TextEditingController(); @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _emailController.dispose(); super.dispose(); } String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Please enter an email address'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email address'; } return null; } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ CustomFormField( label: 'Username', controller: _usernameController, prefixIcon: Icons.person, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a username'; } if (value.length < 3) { return 'Username must be at least 3 characters long'; } return null; }, ), SizedBox(height: 16), CustomFormField( label: 'Email', controller: _emailController, prefixIcon: Icons.email, validator: _validateEmail, ), SizedBox(height: 16), CustomFormField( label: 'Password', controller: _passwordController, obscureText: true, prefixIcon: Icons.lock, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a password'; } if (value.length < 6) { return 'Password must be at least 6 characters long'; } return null; }, ), SizedBox(height: 24), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Handle form submission print('Form submitted successfully'); } }, child: Text('Submit'), ), ], ), ); } }
Best Practices for Form Handling
- Always Validate Input: Implement proper validation for all form fields to ensure data integrity.
- Use Appropriate Widgets: Choose the right widget for each input type (e.g.,
TextFormField
for text,CheckboxListTile
for boolean values). - Provide Clear Feedback: Show error messages and validation feedback to users.
- Handle Form State: Use
FormState
to manage form validation and submission. - Clean Up Resources: Always dispose of controllers when they're no longer needed.
- Consider Accessibility: Ensure your forms are accessible by providing proper labels and semantic information.
- Implement Loading States: Show loading indicators during form submission.
- Handle Errors Gracefully: Provide clear error messages and recovery options.
Conclusion
Flutter provides a rich set of form widgets that make it easy to create beautiful and functional forms. By following best practices and using the appropriate widgets for each use case, you can create forms that are both user-friendly and maintainable. Remember to validate input, provide clear feedback, and handle form state properly to ensure a great user experience.