In mobile application development, forms are essential for collecting user input. Whether it's for login/registration, personal information entry, or data submission, form components are indispensable. Flutter provides a comprehensive form handling mechanism, including form widgets, input validation, and data processing capabilities. This lesson will detailedly introduce form and input-related components in Flutter, helping you build fully functional and user-friendly form interfaces.
I. Basic Form Components
Flutter provides the Form widget as a container for forms, which works with various input controls (such as TextFormField) to implement complete form functionality. The Form widget itself doesn't render any visible content; it's primarily used for managing the state, validation, and submission of form fields.
1. Form Widget and GlobalKey
The Form widget needs a GlobalKey to manage form state, enabling form validation and data submission. A GlobalKey is a way to access state across widgets, uniquely identifying a widget and retrieving its state.
Basic usage example:
class BasicFormExample extends StatefulWidget { const BasicFormExample({super.key}); @override State<BasicFormExample> createState() => _BasicFormExampleState(); } class _BasicFormExampleState extends State<BasicFormExample> { // Create a global key for the form final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Basic Form')), body: Padding( padding: const EdgeInsets.all(16.0), // Form widget child: Form( key: _formKey, // Associate with the global key autovalidateMode: AutovalidateMode.onUserInteraction, // Validation mode child: Column( children: [ // Form field TextFormField( decoration: const InputDecoration( labelText: 'Username', border: OutlineInputBorder(), ), // Validator validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your username'; } return null; // Validation passed }, ), const SizedBox(height: 16), ElevatedButton( onPressed: () { // Validate the form if (_formKey.currentState!.validate()) { // Validation passed, process data ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Processing data')), ); } }, child: const Text('Submit'), ), ], ), ), ), ); } }
Core parameters of the Form widget:
- key: A GlobalKey used to access the form state
- autovalidateMode: Automatic validation mode, determining when to validate input automatically
- AutovalidateMode.disabled: Disable automatic validation (default)
- AutovalidateMode.always: Always validate automatically
- AutovalidateMode.onUserInteraction: Validate on user interaction (e.g., input, focus change)
2. TextFormField Widget
TextFormField is the most commonly used input widget in forms. It inherits from TextField and adds form validation capabilities. It supports various input types (text, numbers, email, etc.) and can be customized in appearance and behavior.
Example of common properties:
TextFormField( // Input box decoration decoration: InputDecoration( labelText: 'Email', hintText: 'Enter your email address', prefixIcon: const Icon(Icons.email), border: const OutlineInputBorder(), // Error提示样式 errorBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.red), borderRadius: BorderRadius.circular(4), ), ), // Input type keyboardType: TextInputType.emailAddress, // Input formatters (can restrict input content) inputFormatters: [ FilteringTextInputFormatter.deny(RegExp(r'\s')), // Deny spaces ], // Text capitalization textCapitalization: TextCapitalization.none, // Password obfuscation obscureText: false, // Set to true for password fields // Autocorrect autocorrect: false, // Autofocus autofocus: false, // Maximum length maxLength: 50, // Maximum lines maxLines: 1, // Validator validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } // Simple email format validation if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; }, // Input change callback onChanged: (value) { print('Email changed: $value'); }, // Input completion callback onFieldSubmitted: (value) { print('Email submitted: $value'); }, )
3. Other Input Widgets
In addition to text input, Flutter provides other commonly used form input widgets:
- Checkbox: For selecting multiple options
- Radio: For selecting one option from multiple choices
- Switch: For turning a feature on/off
- DropdownButton: For selecting from preset options
These widgets can be wrapped with FormField to integrate into forms:
// Checkbox form field FormField<bool>( initialValue: false, validator: (value) { if (value == false) { return 'Please agree to the terms'; } return null; }, builder: (formFieldState) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Checkbox( value: formFieldState.value, onChanged: (value) { formFieldState.didChange(value); }, ), const Text('I agree to the terms and conditions'), ], ), if (formFieldState.hasError) Padding( padding: const EdgeInsets.only(top: 4, left: 4), child: Text( formFieldState.errorText!, style: const TextStyle( color: Colors.red, fontSize: 12, ), ), ), ], ); }, ) // Dropdown selection box FormField<String>( initialValue: 'male', validator: (value) { if (value == null || value.isEmpty) { return 'Please select gender'; } return null; }, builder: (formFieldState) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButton<String>( value: formFieldState.value, isExpanded: true, items: const [ DropdownMenuItem(value: 'male', child: Text('Male')), DropdownMenuItem(value: 'female', child: Text('Female')), DropdownMenuItem(value: 'other', child: Text('Other')), ], onChanged: (value) { formFieldState.didChange(value); }, ), if (formFieldState.hasError) Padding( padding: const EdgeInsets.only(top: 4, left: 4), child: Text( formFieldState.errorText!, style: const TextStyle( color: Colors.red, fontSize: 12, ), ), ), ], ); }, )
II. Input Validation and Form Submission
Form validation is crucial for ensuring user input meets requirements. Flutter provides a flexible validation mechanism that can implement various validation logic from simple to complex.
1. Basic Validation Logic
Each TextFormField can specify a validation function through the validator property. This function receives the input value and returns an error message (validation failed) or null (validation succeeded).
Examples of common validations:
// Username validation validator: (value) { if (value == null || value.isEmpty) { return 'Please enter username'; } if (value.length < 3) { return 'Username must be at least 3 characters'; } if (value.length > 20) { return 'Username cannot exceed 20 characters'; } return null; } // Password validation validator: (value) { if (value == null || value.isEmpty) { return 'Please enter password'; } if (value.length < 6) { return 'Password must be at least 6 characters'; } if (!RegExp(r'[A-Z]').hasMatch(value)) { return 'Password must contain at least one uppercase letter'; } return null; } // Confirm password validation (needs comparison with password field) validator: (value) { if (value == null || value.isEmpty) { return 'Please confirm password'; } if (value != _passwordController.text) { return 'Passwords do not match'; } return null; }
2. Form Submission and Data Processing
Form validation and data retrieval can be triggered through GlobalKey:
class FormSubmissionExample extends StatefulWidget { const FormSubmissionExample({super.key}); @override State<FormSubmissionExample> createState() => _FormSubmissionExampleState(); } class _FormSubmissionExampleState extends State<FormSubmissionExample> { final _formKey = GlobalKey<FormState>(); final _usernameController = TextEditingController(); final _emailController = TextEditingController(); @override void dispose() { // Release controller resources _usernameController.dispose(); _emailController.dispose(); super.dispose(); } // Submit the form void _submitForm() { // Validate all fields if (_formKey.currentState!.validate()) { // Validation passed, save form state _formKey.currentState!.save(); // Get input values final username = _usernameController.text; final email = _emailController.text; // Process form data (e.g., submit to server) _processFormData(username, email); } } // Process form data void _processFormData(String username, String email) { print('Username: $username'); print('Email: $email'); // Show submission success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Form submitted successfully')), ); // Can navigate to other pages here // Navigator.pushNamed(context, '/success'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Form Submission')), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter username'; } if (value.length < 3) { return 'Username must be at least 3 characters'; } return null; }, // Save callback (triggered when save() is called) onSaved: (value) { print('Username saved: $value'); }, ), const SizedBox(height: 16), TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter email'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; }, ), const SizedBox(height: 24), ElevatedButton( onPressed: _submitForm, child: const Text('Submit'), ), ], ), ), ), ); } }
Form processing flow:
- User enters data
- Validation is triggered (manually or automatically)
- User clicks the submit button
- Call _formKey.currentState!.validate() to validate all fields
- After successful validation, call _formKey.currentState!.save() to save all field values
- Retrieve input data and process it (e.g., submit to server)
3. Manually Controlling Validation Timing
In addition to automatic validation, you can also manually control when validation occurs:
// Only validate on submission Form( key: _formKey, autovalidateMode: AutovalidateMode.disabled, // Disable automatic validation // ... ) // Manually trigger validation for a single field void _validateUsername() { _usernameFieldKey.currentState?.validate(); } // Reset the form void _resetForm() { _formKey.currentState?.reset(); }
III. Text Controller: TextEditingController
TextEditingController is used to control the content of text input widgets. It can retrieve, set, and listen to changes in input content, making it an important tool for handling form data.
1. Basic Usage
class TextEditingControllerExample extends StatefulWidget { const TextEditingControllerExample({super.key}); @override State<TextEditingControllerExample> createState() => _TextEditingControllerExampleState(); } class _TextEditingControllerExampleState extends State<TextEditingControllerExample> { // Create text controller final _controller = TextEditingController(); @override void initState() { super.initState(); // Set initial value _controller.text = 'Initial value'; // Listen to text changes _controller.addListener(_onTextChanged); } @override void dispose() { // Remove listener and release resources _controller.removeListener(_onTextChanged); _controller.dispose(); super.dispose(); } // Text change callback void _onTextChanged() { print('Text changed: ${_controller.text}'); } // Get input value void _getValue() { final text = _controller.text; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Current value: $text')), ); } // Set input value void _setValue() { _controller.text = 'New value set programmatically'; } // Clear input void _clearValue() { _controller.clear(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Text Controller')), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _controller, // Associate with controller decoration: const InputDecoration( labelText: 'Enter text', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( onPressed: _getValue, child: const Text('Get Value'), ), ElevatedButton( onPressed: _setValue, child: const Text('Set Value'), ), ElevatedButton( onPressed: _clearValue, child: const Text('Clear'), ), ], ), ], ), ), ); } }
2. Advanced Usage
TextEditingController also provides some advanced features:
- Selecting text:
// Select text _controller.selection = TextSelection( baseOffset: 2, extentOffset: 5, ); // Select all text _controller.selection = TextSelection( baseOffset: 0, extentOffset: _controller.text.length, );
- Setting cursor position:
// Set cursor position to the end _controller.selection = TextSelection.fromPosition( TextPosition(offset: _controller.text.length), );
- Styled text: Using TextSpan to set rich text:
final _richTextController = TextEditingController( text: 'Hello World', ); // In build method TextField( controller: _richTextController, decoration: const InputDecoration(labelText: 'Rich Text'), style: const TextStyle(fontSize: 16), // Can restrict input format through inputFormatters )
IV. Example: Implementing Login and Registration Forms
The following implements complete login and registration forms with features including input validation, form submission, and password show/hide toggle.
1. Login Form
class LoginForm extends StatefulWidget { const LoginForm({super.key}); @override State<LoginForm> createState() => _LoginFormState(); } class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _obscurePassword = true; // Controls whether password is visible bool _isLoading = false; // Controls loading state @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } // Toggle password visibility void _togglePasswordVisibility() { setState(() { _obscurePassword = !_obscurePassword; }); } // Submit login form Future<void> _submitLogin() async { if (_formKey.currentState!.validate()) { setState(() { _isLoading = true; }); // Simulate login request try { await Future.delayed(const Duration(seconds: 2)); // Login successful, navigate to home page if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login successful')), ); // Navigator.pushReplacementNamed(context, '/home'); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Login failed: $e')), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } } @override Widget build(BuildContext context) { return Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email), border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock), border: const OutlineInputBorder(), // Password visibility toggle button suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off, ), onPressed: _togglePasswordVisibility, ), ), obscureText: _obscurePassword, enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } if (value.length < 6) { return 'Password must be at least 6 characters'; } return null; }, ), const SizedBox(height: 8), // Forgot password link Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { // Navigate to forgot password page // Navigator.pushNamed(context, '/forgot-password'); }, child: const Text('Forgot Password?'), ), ), const SizedBox(height: 16), // Login button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submitLogin, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Text('Login'), ), ), const SizedBox(height: 16), // Register link Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Don't have an account?"), TextButton( onPressed: () { // Navigate to register page // Navigator.pushNamed(context, '/register'); }, child: const Text('Register'), ), ], ), ], ), ); } }
2. Registration Form
class RegisterForm extends StatefulWidget { const RegisterForm({super.key}); @override State<RegisterForm> createState() => _RegisterFormState(); } class _RegisterFormState extends State<RegisterForm> { final _formKey = GlobalKey<FormState>(); final _usernameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); bool _obscurePassword = true; bool _obscureConfirmPassword = true; bool _isLoading = false; String? _selectedGender; @override void dispose() { _usernameController.dispose(); _emailController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } void _togglePasswordVisibility() { setState(() { _obscurePassword = !_obscurePassword; }); } void _toggleConfirmPasswordVisibility() { setState(() { _obscureConfirmPassword = !_obscureConfirmPassword; }); } Future<void> _submitRegistration() async { if (_formKey.currentState!.validate()) { setState(() { _isLoading = true; }); // Simulate registration request try { await Future.delayed(const Duration(seconds: 2)); // Registration successful if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Registration successful')), ); Navigator.pop(context); // Return to login page } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Registration failed: $e')), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } } @override Widget build(BuildContext context) { return Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: SingleChildScrollView( child: Column( children: [ TextFormField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', prefixIcon: Icon(Icons.person), border: OutlineInputBorder(), ), enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your username'; } if (value.length < 3) { return 'Username must be at least 3 characters'; } if (value.length > 20) { return 'Username cannot exceed 20 characters'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email), border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; }, ), const SizedBox(height: 16), // Gender selection DropdownButtonFormField<String>( value: _selectedGender, decoration: const InputDecoration( labelText: 'Gender', prefixIcon: Icon(Icons.person_2), border: OutlineInputBorder(), ), items: const [ DropdownMenuItem(value: 'male', child: Text('Male')), DropdownMenuItem(value: 'female', child: Text('Female')), DropdownMenuItem(value: 'other', child: Text('Other')), ], onChanged: !_isLoading ? (value) { setState(() { _selectedGender = value; }); } : null, validator: (value) { if (value == null) { return 'Please select your gender'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock), border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off, ), onPressed: _togglePasswordVisibility, ), ), obscureText: _obscurePassword, enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } if (value.length < 6) { return 'Password must be at least 6 characters'; } if (!RegExp(r'[A-Z]').hasMatch(value)) { return 'Password must contain at least one uppercase letter'; } if (!RegExp(r'[0-9]').hasMatch(value)) { return 'Password must contain at least one number'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _confirmPasswordController, decoration: InputDecoration( labelText: 'Confirm Password', prefixIcon: const Icon(Icons.lock_clock), border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _obscureConfirmPassword ? Icons.visibility : Icons.visibility_off, ), onPressed: _toggleConfirmPasswordVisibility, ), ), obscureText: _obscureConfirmPassword, enabled: !_isLoading, validator: (value) { if (value == null || value.isEmpty) { return 'Please confirm your password'; } if (value != _passwordController.text) { return 'Passwords do not match'; } return null; }, ), const SizedBox(height: 16), // Agree to terms FormField<bool>( initialValue: false, validator: (value) { if (value == false) { return 'Please agree to the terms'; } return null; }, builder: (formFieldState) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Checkbox( value: formFieldState.value, onChanged: _isLoading ? null : (value) { formFieldState.didChange(value); }, ), const Text('I agree to the Terms of Service and Privacy Policy'), ], ), if (formFieldState.hasError) Padding( padding: const EdgeInsets.only(top: 4, left: 4), child: Text( formFieldState.errorText!, style: const TextStyle( color: Colors.red, fontSize: 12, ), ), ), ], ); }, ), const SizedBox(height: 24), // Register button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submitRegistration, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Text('Register'), ), ), ], ), ), ); } }
3. Form Page Integration
class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @override State<AuthScreen> createState() => _AuthScreenState(); } class _AuthScreenState extends State<AuthScreen> { bool _isLogin = true; // Toggle between login/register modes // Toggle form mode void _toggleFormMode() { setState(() { _isLogin = !_isLogin; }); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Title Text( _isLogin ? 'Welcome Back' : 'Create Account', style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( _isLogin ? 'Sign in to your account to continue' : 'Fill in the details to create a new account', style: const TextStyle( color: Colors.grey, fontSize: 16, ), ), const SizedBox(height: 32), // Show login or register form _isLogin ? const LoginForm() : const RegisterForm(), // Toggle form button if (!_isLogin) TextButton( onPressed: _toggleFormMode, child: const Text('Already have an account? Login'), ), ], ), ), ); } }
Top comments (0)