4.1Formulaires et saisie utilisateur
4.1.1 – Champs de saisie texte
TextField est le widget principal pour permettre à l'utilisateur de saisir du texte. C'est l'équivalent d'un champ de formulaire HTML.
📝 Qu'est-ce qu'un TextField ?
TextField affiche un champ de saisie où l'utilisateur peut taper du texte. Il est utilisé pour collecter des informations comme un nom, un email, un mot de passe, etc.
TextField est comme un champ de formulaire sur une page web. L'utilisateur clique dedans, tape du texte, et vous pouvez récupérer ce texte dans votre code.
📝 Syntaxe
Voici la syntaxe de base de TextField :
TextField(
decoration: InputDecoration(
labelText: 'Votre nom',
border: OutlineInputBorder(),
),
onChanged: (value) {
// Code exécuté quand le texte change
},
)
Analysons cette syntaxe :
decoration: InputDecoration(...): Définit l'apparence du champ (label, bordure, etc.)labelText: Le texte d'étiquette qui apparaît dans le champborder: OutlineInputBorder(): Type de bordure (contour)onChanged: (value) { ... }: Fonction appelée quand le texte change
🧪 Exemple : TextField simple
Voici un exemple simple avec un TextField :
TextField simple sur Flutter.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
String texteSaisi = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TextField'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Votre nom',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
texteSaisi = value;
});
},
),
const SizedBox(height: 20),
Text(
'Vous avez saisi : $texteSaisi',
style: const TextStyle(fontSize: 18),
),
],
),
),
),
);
}
}
Dans cet exemple, quand l'utilisateur tape dans le champ, le texte saisi s'affiche en dessous en temps réel grâce à onChanged et setState().
onChanged est appelé chaque fois que l'utilisateur modifie le texte dans le champ. Le paramètre value contient le nouveau texte saisi.
4.1.2 – Widgets de sélection (Checkbox, Radio, Switch, Dropdown)
Flutter propose plusieurs widgets pour permettre à l'utilisateur de faire des choix : Checkbox pour des cases à cocher, Radio pour des choix uniques, Switch pour activer/désactiver, et DropdownButton pour des listes déroulantes.
☑️ Checkbox
Checkbox permet à l'utilisateur de cocher ou décocher une option. Il est utile pour des choix multiples.
📝 Syntaxe
Checkbox(
value: estCoche,
onChanged: (bool? nouvelleValeur) {
setState(() {
estCoche = nouvelleValeur ?? false;
});
},
)
🧪 Exemple : Checkbox
Voici un exemple avec un Checkbox :
Checkbox simple sur Flutter.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
bool accepterConditions = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Checkbox'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Checkbox(
value: accepterConditions,
onChanged: (bool? valeur) {
setState(() {
accepterConditions = valeur ?? false;
});
},
),
Text(accepterConditions ? 'Conditions acceptées !' : 'J\'accepte les conditions'),
],
),
),
),
);
}
}
🔘 Radio
RadioGroup et RadioListTile permettent à l'utilisateur de choisir une seule option parmi plusieurs. RadioGroup gère l'état du groupe, et RadioListTile affiche chaque option avec un label.
📝 Syntaxe
RadioGroup<String>(
groupValue: choixActuel,
onChanged: (String? valeur) {
setState(() {
choixActuel = valeur;
});
},
child: Column(
children: [
RadioListTile<String>(
title: Text('Option 1'),
value: 'option1',
),
RadioListTile<String>(
title: Text('Option 2'),
value: 'option2',
),
],
),
)
🧪 Exemple : Radio
Voici un exemple avec des boutons Radio :
RadioListTile simple sur Flutter.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
String? choix;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Radio'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RadioGroup<String>(
groupValue: choix,
onChanged: (String? valeur) {
setState(() {
choix = valeur;
});
},
child: Column(
children: const [
RadioListTile<String>(
title: Text('Option 1'),
value: 'option1',
),
RadioListTile<String>(
title: Text('Option 2'),
value: 'option2',
),
RadioListTile<String>(
title: Text('Option 3'),
value: 'option3',
),
],
),
),
const SizedBox(height: 20),
Text(
'Choix sélectionné : ${choix ?? "Aucun"}',
style: const TextStyle(fontSize: 16),
),
],
),
),
),
);
}
}
RadioGroup gère l'état du groupe de boutons radio (via groupValue et onChanged). RadioListTile affiche chaque option avec un titre et gère automatiquement l'interaction avec le groupe.
🔀 Switch
Switch est un interrupteur qui permet d'activer ou désactiver une option. C'est visuellement différent d'un Checkbox.
📝 Syntaxe
Switch(
value: estActive,
onChanged: (bool nouvelleValeur) {
setState(() {
estActive = nouvelleValeur;
});
},
)
🧪 Exemple : Switch
Voici un exemple avec un Switch :
Switch simple sur Flutter.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
bool notificationsActivees = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Switch'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Activer les notifications',
style: TextStyle(fontSize: 18),
),
Switch(
value: notificationsActivees,
onChanged: (bool valeur) {
setState(() {
notificationsActivees = valeur;
});
},
),
],
),
),
),
);
}
}
📋 DropdownButton
DropdownButton affiche une liste déroulante permettant de choisir une option parmi plusieurs.
📝 Syntaxe
DropdownButton<String>(
value: choixActuel,
items: [
DropdownMenuItem(value: 'option1', child: Text('Option 1')),
DropdownMenuItem(value: 'option2', child: Text('Option 2')),
],
onChanged: (String? nouvelleValeur) {
setState(() {
choixActuel = nouvelleValeur;
});
},
)
🧪 Exemple : DropdownButton
Voici un exemple avec un DropdownButton :
DropdownButton pour afficher une liste déroulante de pays.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
String? paysSelectionne = 'Maroc';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DropdownButton'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'Choisissez votre pays :',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
DropdownButton<String>(
value: paysSelectionne,
isExpanded: true,
items: const [
DropdownMenuItem(value: 'Maroc', child: Text('Maroc')),
DropdownMenuItem(value: 'France', child: Text('France')),
DropdownMenuItem(value: 'Espagne', child: Text('Espagne')),
DropdownMenuItem(value: 'Algérie', child: Text('Algérie')),
],
onChanged: (String? nouvelleValeur) {
setState(() {
paysSelectionne = nouvelleValeur;
});
},
),
const SizedBox(height: 20),
Text(
'Pays sélectionné : $paysSelectionne',
style: const TextStyle(fontSize: 18),
),
],
),
),
),
);
}
}
isExpanded: true fait en sorte que le DropdownButton prenne toute la largeur disponible. Sans cela, il ne prend que la largeur nécessaire pour afficher l'élément sélectionné.
4.1.3 – Gestion du formulaire (Form et FormField)
Form est un widget qui regroupe plusieurs champs de formulaire et facilite leur gestion, notamment la validation et la soumission.
📋 Qu'est-ce qu'un Form ?
Form est un conteneur qui regroupe plusieurs champs de formulaire. Il permet de :
- Valider tous les champs en une fois
- Réinitialiser tous les champs
- Gérer l'état de validation global
Form facilite la gestion de formulaires complexes avec plusieurs champs. Au lieu de gérer chaque champ individuellement, vous pouvez valider et soumettre tout le formulaire d'un coup.
📝 Syntaxe
Voici la syntaxe de base pour utiliser Form :
Form(
key: _formKey,
child: Column(
children: [
TextFormField(...),
TextFormField(...),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Formulaire valide
}
},
child: Text('Valider'),
),
],
),
)
🧪 Exemple : Formulaire complet
Voici un exemple complet avec un Form :
Form simple avec deux champs de formulaire.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final _formKey = GlobalKey<FormState>();
final _nomController = TextEditingController();
final _emailController = TextEditingController();
@override
void dispose() {
_nomController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Formulaire'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Nom : ${_nomController.text}');
print('Email : ${_emailController.text}');
}
},
child: const Text('Valider'),
),
],
),
),
),
),
);
}
}
Dans cet exemple, le Form regroupe deux champs. Quand l'utilisateur clique sur "Valider", tous les champs sont validés en une fois grâce à _formKey.currentState!.validate().
TextEditingController permet de contrôler et lire le contenu d'un TextFormField. Vous pouvez accéder au texte avec controller.text.
GlobalKey permet d'accéder à l'état du Form depuis n'importe où. _formKey.currentState!.validate() valide tous les champs du formulaire.
4.1.4 – Validation et soumission des données
La validation permet de vérifier que les données saisies par l'utilisateur sont correctes avant de les soumettre. Flutter facilite la validation avec le paramètre validator des champs de formulaire.
✅ Qu'est-ce que la validation ?
La validation consiste à vérifier que les données saisies respectent certaines règles (champ obligatoire, format email, longueur minimale, etc.). Si les données ne sont pas valides, un message d'erreur est affiché.
📝 Syntaxe
Voici comment ajouter une validation à un champ :
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Ce champ est obligatoire';
}
return null; // null = pas d'erreur
},
)
La fonction validator :
- Reçoit la valeur saisie dans le paramètre
value - Retourne une chaîne d'erreur si la valeur n'est pas valide
- Retourne
nullsi la valeur est valide
🧪 Exemple : Formulaire avec validation
Voici un exemple complet avec validation :
Form avec validation pour un formulaire d'inscription.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final _formKey = GlobalKey<FormState>();
final _nomController = TextEditingController();
final _emailController = TextEditingController();
final _motDePasseController = TextEditingController();
@override
void dispose() {
_nomController.dispose();
_emailController.dispose();
_motDePasseController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Validation'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le nom est obligatoire';
}
if (value.length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'L\'email est obligatoire';
}
if (!value.contains('@')) {
return 'L\'email doit contenir @';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _motDePasseController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le mot de passe est obligatoire';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Tous les champs sont valides
print('Formulaire valide !');
print('Nom : ${_nomController.text}');
print('Email : ${_emailController.text}');
}
},
child: const Text('S\'inscrire'),
),
],
),
),
),
),
);
}
}
Dans cet exemple, chaque champ a sa propre validation :
- Nom : Obligatoire et au moins 2 caractères
- Email : Obligatoire et doit contenir "@"
- Mot de passe : Obligatoire et au moins 6 caractères
obscureText: true masque le texte saisi (utile pour les mots de passe). Les caractères sont remplacés par des points.
La validation ne s'exécute que quand vous appelez
validate() sur le formulaire. Par défaut, elle ne se fait pas automatiquement pendant la saisie.
4.1.5 – Gestion du focus et du clavier
Le focus détermine quel champ est actuellement actif (où le curseur clignote). Vous pouvez contrôler le focus pour améliorer l'expérience utilisateur, par exemple en passant automatiquement au champ suivant.
🎯 Qu'est-ce que le focus ?
Le focus indique quel champ de texte est actuellement actif. Quand un champ a le focus, le clavier apparaît et l'utilisateur peut taper dedans.
FocusNode est un objet qui représente le focus d'un champ. Vous pouvez l'utiliser pour contrôler quel champ a le focus.
📝 Syntaxe
Voici comment gérer le focus :
// Créer un FocusNode
final _focusNode = FocusNode();
// Attacher au TextField
TextField(
focusNode: _focusNode,
onSubmitted: (value) {
// Code exécuté quand l'utilisateur appuie sur Entrée
_focusNode.nextFocus(); // Passe au champ suivant
},
)
// Libérer dans dispose()
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
🧪 Exemple : Gestion du focus
Voici un exemple qui montre comment gérer le focus et passer automatiquement au champ suivant :
Form avec focus pour un formulaire d'inscription.
import 'package:flutter/material.dart';
void main() {
runApp(const MonApp());
}
class MonApp extends StatelessWidget {
const MonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const MaPage(),
);
}
}
class MaPage extends StatefulWidget {
const MaPage({super.key});
@override
State<MaPage> createState() => _MaPageState();
}
class _MaPageState extends State<MaPage> {
final _nomFocus = FocusNode();
final _emailFocus = FocusNode();
final _nomController = TextEditingController();
final _emailController = TextEditingController();
@override
void dispose() {
_nomFocus.dispose();
_emailFocus.dispose();
_nomController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gestion du focus'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _nomController,
focusNode: _nomFocus,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
onSubmitted: (value) {
_nomFocus.unfocus();
_emailFocus.requestFocus();
},
),
const SizedBox(height: 16),
TextField(
controller: _emailController,
focusNode: _emailFocus,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.done,
keyboardType: TextInputType.emailAddress,
onSubmitted: (value) {
_emailFocus.unfocus();
},
),
],
),
),
),
);
}
}
Dans cet exemple :
- Quand l'utilisateur appuie sur "Suivant" dans le champ Nom, le focus passe automatiquement au champ Email
textInputAction: TextInputAction.nextaffiche un bouton "Suivant" sur le claviertextInputAction: TextInputAction.doneaffiche un bouton "Terminé" sur le clavierkeyboardType: TextInputType.emailAddressaffiche un clavier optimisé pour les emails
textInputAction définit le bouton affiché sur le clavier :
TextInputAction.next: Bouton "Suivant"TextInputAction.done: Bouton "Terminé"TextInputAction.search: Bouton "Rechercher"
keyboardType définit le type de clavier affiché :
TextInputType.emailAddress: Clavier avec @TextInputType.phone: Clavier numériqueTextInputType.number: Clavier numériqueTextInputType.text: Clavier texte standard