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)