Flutter Forms and Validation
This section will introduce the creation, validation, and submission handling of forms in Flutter.
TextFormField - Form Input
TextFormField is a TextField specifically designed for forms, with built-in validation support.
Example: Basic Form
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
// Form key (for validation)
final _formKey = GlobalKey<FormState>();
// Input controllers
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// Username input
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: Username,
prefixIcon: Icon(Icons.person),
),
// Validator
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;
},
),
const SizedBox(height: 16),
// Password input
TextFormField(
controller: _passwordController,
obscureText: true, // Hide password
decoration: const InputDecoration(
labelText: Password,
prefixIcon: Icon(Icons.lock),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 24),
// Submit button
ElevatedButton(
onPressed: _submit,
child: const Text('Login'),
),
],
),
);
}
void _submit() {
// Validate form
if (_formKey.currentState?.validate() ?? false) {
// Validation passed
print('Username: ${_usernameController.text}');
print('Password: ${_passwordController.text}');
// Submit to server...
} else {
// Validation failed
print('Form validation failed');
}
}
}
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
// Form key (for validation)
final _formKey = GlobalKey<FormState>();
// Input controllers
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// Username input
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: Username,
prefixIcon: Icon(Icons.person),
),
// Validator
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;
},
),
const SizedBox(height: 16),
// Password input
TextFormField(
controller: _passwordController,
obscureText: true, // Hide password
decoration: const InputDecoration(
labelText: Password,
prefixIcon: Icon(Icons.lock),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 24),
// Submit button
ElevatedButton(
onPressed: _submit,
child: const Text('Login'),
),
],
),
);
}
void _submit() {
// Validate form
if (_formKey.currentState?.validate() ?? false) {
// Validation passed
print('Username: ${_usernameController.text}');
print('Password: ${_passwordController.text}');
// Submit to server...
} else {
// Validation failed
print('Form validation failed');
}
}
}
Common validators
Example: Common Validation Rules
// Validate username
String? validateUsername(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter username';
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
return 'Can only contain letters, numbers, and underscores';
}
return null;
}
// Validate email
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter email';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return Please enter a valid email address;
}
return null;
}
// Validate password strength
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
if (value.length < 8) {
return 'Password must be at least 8 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;
}
// Validate phone number
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter phone number';
}
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(value)) {
return 'Please enter a valid phone number';
}
return null;
}
// Validate passwords match
String? validateConfirmPassword(String? value, String password) {
if (value == null || value.isEmpty) {
return 'Please confirm password';
}
if (value != password) {
return 'The two passwords do not match';
}
return null;
}
String? validateUsername(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter username';
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
return 'Can only contain letters, numbers, and underscores';
}
return null;
}
// Validate email
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter email';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return Please enter a valid email address;
}
return null;
}
// Validate password strength
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
if (value.length < 8) {
return 'Password must be at least 8 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;
}
// Validate phone number
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter phone number';
}
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(value)) {
return 'Please enter a valid phone number';
}
return null;
}
// Validate passwords match
String? validateConfirmPassword(String? value, String password) {
if (value == null || value.isEmpty) {
return 'Please confirm password';
}
if (value != password) {
return 'The two passwords do not match';
}
return null;
}
Complex Form Example
Example: 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 _phoneController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _acceptTerms = false;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Username
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: Username),
validator: validateUsername,
),
const SizedBox(height: 16),
// Email
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(labelText: Email),
validator: validateEmail,
),
const SizedBox(height: 16),
// Phone number
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(labelText: 'Phone number'),
validator: validatePhone,
),
const SizedBox(height: 16),
// Password
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: Password),
validator: validatePassword,
),
const SizedBox(height: 16),
// Confirm password
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(labelText: Confirm password),
validator: (value) => validateConfirmPassword(
value,
_passwordController.text,
),
),
const SizedBox(height: 16),
// Agree to terms
CheckboxListTile(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
title: const Text(I agree to the Terms of Service),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 24),
// Submit button
ElevatedButton(
onPressed: _acceptTerms ? _submit : null,
child: const Text(Register),
),
],
),
);
}
void _submit() {
if (_formKey.currentState?.validate() ?? false) {
// Submit form
print(Registration successful!);
}
}
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
}
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 _phoneController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _acceptTerms = false;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Username
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: Username),
validator: validateUsername,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(labelText: Email),
validator: validateEmail,
),
const SizedBox(height: 16),
// Phone number
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(labelText: 'Phone number'),
validator: validatePhone,
),
const SizedBox(height: 16),
// Password
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: Password),
validator: validatePassword,
),
const SizedBox(height: 16),
// Confirm password
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(labelText: Confirm password),
validator: (value) => validateConfirmPassword(
value,
_passwordController.text,
),
),
const SizedBox(height: 16),
// Agree to terms
CheckboxListTile(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
title: const Text(I agree to the Terms of Service),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 24),
// Submit button
ElevatedButton(
onPressed: _acceptTerms ? _submit : null,
child: const Text(Register),
),
],
),
);
}
void _submit() {
if (_formKey.currentState?.validate() ?? false) {
// Submit form
print(Registration successful!);
}
}
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
}
other extensionsForm validation should be performed on both the client side (for user experience) and the server side (for security); do not rely solely on client-side validation.