Formulaires et Validation
Form : Widget de formulaire
Le widget Form permet de regrouper plusieurs champs de saisie et de gérer leur validation de manière centralisée.
Formulaire basique
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Formulaire basique')),
body: const Padding(
padding: EdgeInsets.all(16),
child: MyForm(),
),
),
);
}
}
class MyForm extends StatefulWidget {
const MyForm({super.key});
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
String _name = '';
String _email = '';
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Nom'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom';
}
return null;
},
onSaved: (value) {
_name = value!;
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un email';
}
if (!value.contains('@')) {
return 'Email invalide';
}
return null;
},
onSaved: (value) {
_email = value!;
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
debugPrint('Nom: $_name, Email: $_email');
}
},
child: const Text('Soumettre'),
),
],
),
);
}
}
TextFormField : Champ de texte avec validation
Types de clavier
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Types de clavier')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
TextFormField(
decoration: InputDecoration(labelText: 'Téléphone'),
keyboardType: TextInputType.phone,
),
SizedBox(height: 12),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 12),
TextFormField(
decoration: InputDecoration(labelText: 'Nombre'),
keyboardType: TextInputType.number,
),
SizedBox(height: 12),
TextFormField(
decoration: InputDecoration(labelText: 'URL'),
keyboardType: TextInputType.url,
),
],
),
),
);
}
}
Masquer le texte (mots de passe)
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: PasswordFormPage(),
);
}
}
class PasswordFormPage extends StatefulWidget {
const PasswordFormPage({super.key});
State<PasswordFormPage> createState() => _PasswordFormPageState();
}
class _PasswordFormPageState extends State<PasswordFormPage> {
final _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mot de passe')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
const PasswordField(),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mot de passe valide')),
);
}
},
child: const Text('Valider'),
),
],
),
),
),
);
}
}
class PasswordField extends StatefulWidget {
const PasswordField({super.key});
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscureText = true;
Widget build(BuildContext context) {
return TextFormField(
obscureText: _obscureText,
decoration: InputDecoration(
labelText: 'Mot de passe',
suffixIcon: IconButton(
icon: Icon(_obscureText ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
validator: (value) {
if (value == null || value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
);
}
}
Limitation de caractères
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Limitation de caractères')),
body: const Padding(
padding: EdgeInsets.all(16),
child: TextFormField(
maxLength: 100,
maxLines: 3,
decoration: InputDecoration(
labelText: 'Description',
helperText: 'Maximum 100 caractères',
),
),
),
),
);
}
}
Validation avancée
Validateurs personnalisés
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: ValidatorsDemo());
}
}
class ValidatorsDemo extends StatefulWidget {
const ValidatorsDemo({super.key});
State<ValidatorsDemo> createState() => _ValidatorsDemoState();
}
class _ValidatorsDemoState extends State<ValidatorsDemo> {
final _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Validateurs personnalisés')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: validateEmail,
),
const SizedBox(height: 12),
TextFormField(
decoration: const InputDecoration(labelText: 'Téléphone'),
keyboardType: TextInputType.phone,
validator: validatePhone,
),
const SizedBox(height: 12),
TextFormField(
decoration: const InputDecoration(labelText: 'Mot de passe'),
obscureText: true,
validator: validatePassword,
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Formulaire valide')),
);
}
},
child: const Text('Valider'),
),
],
),
),
);
}
}
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email requis';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
}
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'Numéro de téléphone requis';
}
final phoneRegex = RegExp(r'^\d{10}$');
if (!phoneRegex.hasMatch(value)) {
return 'Format: 10 chiffres';
}
return null;
}
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Mot de passe requis';
}
if (value.length < 8) {
return 'Minimum 8 caractères';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Au moins une majuscule requise';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Au moins un chiffre requis';
}
return null;
}
Validation conditionnelle
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: ConditionalFormPage());
}
}
class ConditionalFormPage extends StatelessWidget {
const ConditionalFormPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Validation conditionnelle')),
body: const Padding(
padding: EdgeInsets.all(16),
child: ConditionalForm(),
),
);
}
}
class ConditionalForm extends StatefulWidget {
const ConditionalForm({super.key});
State<ConditionalForm> createState() => _ConditionalFormState();
}
class _ConditionalFormState extends State<ConditionalForm> {
final _formKey = GlobalKey<FormState>();
bool _hasCompany = false;
String _companyName = '';
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
CheckboxListTile(
title: const Text('J\'ai une entreprise'),
value: _hasCompany,
onChanged: (value) {
setState(() {
_hasCompany = value ?? false;
});
},
),
if (_hasCompany)
TextFormField(
decoration: const InputDecoration(labelText: 'Nom de l\'entreprise'),
validator: (value) {
if (_hasCompany && (value == null || value.isEmpty)) {
return 'Nom d\'entreprise requis';
}
return null;
},
onSaved: (value) {
_companyName = value ?? '';
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
debugPrint('Entreprise: $_companyName');
}
},
child: const Text('Valider'),
),
],
),
);
}
}
Gestion des contrôleurs
TextEditingController
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: ControllerDemo());
}
}
class ControllerDemo extends StatelessWidget {
const ControllerDemo({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TextEditingController')),
body: const Padding(
padding: EdgeInsets.all(16),
child: ControllerExample(),
),
);
}
}
class ControllerExample extends StatefulWidget {
const ControllerExample({super.key});
State<ControllerExample> createState() => _ControllerExampleState();
}
class _ControllerExampleState extends State<ControllerExample> {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
void initState() {
super.initState();
// Valeurs par défaut
_nameController.text = 'John Doe';
// Écouter les changements
_nameController.addListener(() {
debugPrint('Nom changé: ${_nameController.text}');
});
}
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
void _clearForm() {
_nameController.clear();
_emailController.clear();
}
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Nom'),
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _clearForm,
child: const Text('Effacer'),
),
],
);
}
}
Focus et navigation entre champs
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: FocusDemo());
}
}
class FocusDemo extends StatelessWidget {
const FocusDemo({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Focus et navigation')),
body: const Padding(
padding: EdgeInsets.all(16),
child: FocusExample(),
),
);
}
}
class FocusExample extends StatefulWidget {
const FocusExample({super.key});
State<FocusExample> createState() => _FocusExampleState();
}
class _FocusExampleState extends State<FocusExample> {
final _nameFocus = FocusNode();
final _emailFocus = FocusNode();
final _phoneFocus = FocusNode();
void dispose() {
_nameFocus.dispose();
_emailFocus.dispose();
_phoneFocus.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
focusNode: _nameFocus,
decoration: const InputDecoration(labelText: 'Nom'),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_emailFocus);
},
),
TextFormField(
focusNode: _emailFocus,
decoration: const InputDecoration(labelText: 'Email'),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_phoneFocus);
},
),
TextFormField(
focusNode: _phoneFocus,
decoration: const InputDecoration(labelText: 'Téléphone'),
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) {
_phoneFocus.unfocus();
},
),
],
);
}
}
Formulaire complet
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(home: CompleteFormPage());
}
}
class CompleteFormPage extends StatelessWidget {
const CompleteFormPage({super.key});
Widget build(BuildContext context) {
return const Scaffold(
appBar: AppBar(title: Text('Formulaire complet')),
body: CompleteForm(),
);
}
}
class CompleteForm extends StatefulWidget {
const CompleteForm({super.key});
State<CompleteForm> createState() => _CompleteFormState();
}
class _CompleteFormState extends State<CompleteForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
String _selectedCountry = 'Canada';
bool _acceptsTerms = false;
bool _isLoading = false;
void dispose() {
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
super.dispose();
}
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptsTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Vous devez accepter les conditions'),
),
);
return;
}
setState(() {
_isLoading = true;
});
// Simuler une requête
await Future.delayed(const Duration(seconds: 2));
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Formulaire soumis avec succès')),
);
}
}
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Nom complet',
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nom requis';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: validateEmail,
),
const SizedBox(height: 16),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Téléphone',
prefixIcon: Icon(Icons.phone),
),
keyboardType: TextInputType.phone,
validator: validatePhone,
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedCountry,
decoration: const InputDecoration(
labelText: 'Pays',
prefixIcon: Icon(Icons.flag),
),
items: ['Canada', 'France', 'Belgique', 'Suisse']
.map((country) => DropdownMenuItem(
value: country,
child: Text(country),
))
.toList(),
onChanged: (value) {
setState(() {
_selectedCountry = value!;
});
},
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('J\'accepte les conditions d\'utilisation'),
value: _acceptsTerms,
onChanged: (value) {
setState(() {
_acceptsTerms = value ?? false;
});
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Soumettre'),
),
],
),
);
}
}
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email requis';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
}
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'Numéro de téléphone requis';
}
final phoneRegex = RegExp(r'^\d{10}$');
if (!phoneRegex.hasMatch(value)) {
return 'Format: 10 chiffres';
}
return null;
}
Bonnes pratiques
- GlobalKey pour valider et sauvegarder le formulaire
- dispose() pour libérer les contrôleurs
- Validation côté client avant soumission
- FocusNode pour gérer la navigation entre champs
- TextInputAction pour définir l'action du bouton clavier
- Feedback utilisateur avec SnackBar ou Dialog
- État de chargement pendant les soumissions asynchrones