CHAPITRE 4.1

Formulaires et saisie utilisateur

Créez des formulaires interactifs et collectez les données des utilisateurs
Les formulaires sont essentiels dans la plupart des applications : connexion, inscription, recherche, paramètres, etc. Dans ce chapitre, nous allons découvrir comment créer des formulaires complets avec Flutter : champs de texte, widgets de sélection, validation, et gestion du focus.

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.

Analogie :
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 champ
  • border: OutlineInputBorder() : Type de bordure (contour)
  • onChanged: (value) { ... } : Fonction appelée quand le texte change

🧪 Exemple : TextField simple

Voici un exemple simple avec un TextField :

Exemple TextField Flutter
Un champ de saisie 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 :
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 :

Exemple Checkbox Flutter
Un 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 :

Exemple Radio Flutter
Un groupe de boutons 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 et RadioListTile :
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 :

Exemple Switch Flutter
Un 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 :

Exemple DropdownButton Flutter
L'utilisation de 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 :
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
Pourquoi utiliser Form ?
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 :

Exemple Form Flutter
Un 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 :
TextEditingController permet de contrôler et lire le contenu d'un TextFormField. Vous pouvez accéder au texte avec controller.text.
GlobalKey<FormState> :
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 null si la valeur est valide

🧪 Exemple : Formulaire avec validation

Voici un exemple complet avec validation :

Exemple Form Flutter avec validation
L'utilisation de 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 :
obscureText: true masque le texte saisi (utile pour les mots de passe). Les caractères sont remplacés par des points.
Important :
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 :
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 :

Exemple Form Flutter avec focus
L'utilisation de 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.next affiche un bouton "Suivant" sur le clavier
  • textInputAction: TextInputAction.done affiche un bouton "Terminé" sur le clavier
  • keyboardType: TextInputType.emailAddress affiche un clavier optimisé pour les emails
textInputAction :
textInputAction définit le bouton affiché sur le clavier :
  • TextInputAction.next : Bouton "Suivant"
  • TextInputAction.done : Bouton "Terminé"
  • TextInputAction.search : Bouton "Rechercher"
keyboardType :
keyboardType définit le type de clavier affiché :
  • TextInputType.emailAddress : Clavier avec @
  • TextInputType.phone : Clavier numérique
  • TextInputType.number : Clavier numérique
  • TextInputType.text : Clavier texte standard