Aller au contenu principal

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

  1. GlobalKey pour valider et sauvegarder le formulaire
  2. dispose() pour libérer les contrôleurs
  3. Validation côté client avant soumission
  4. FocusNode pour gérer la navigation entre champs
  5. TextInputAction pour définir l'action du bouton clavier
  6. Feedback utilisateur avec SnackBar ou Dialog
  7. État de chargement pendant les soumissions asynchrones