Formulaires et saisie de données Flutter
Introduction aux formulaires Flutter
Section titled “Introduction aux formulaires Flutter”Flutter offre un système complet pour créer des formulaires interactifs et gérer la saisie de données. Ce guide couvre tous les aspects des formulaires, de la création de champs de texte simples aux validations complexes.
TextField et TextEditingController
Section titled “TextField et TextEditingController”TextField de base
Section titled “TextField de base”Le TextField est le widget fondamental pour la saisie de texte. Pour contrôler son contenu, on utilise un TextEditingController.
import 'package:flutter/material.dart';
class MainScreen extends StatefulWidget { const MainScreen({Key? key}) : super(key: key);
@override State<MainScreen> createState() => _MainScreenState();}
class _MainScreenState extends State<MainScreen> { late final TextEditingController nameController; String currentText = '';
@override void initState() { super.initState(); nameController = TextEditingController(); nameController.addListener(() { print('text : ${nameController.text}'); print('selection : ${nameController.selection}'); setState(() => currentText = nameController.text); }); }
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 320), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextField( controller: nameController, decoration: InputDecoration( label: Text('Nom'), ), ), Text('Nom : $currentText') ], ), ), ), ); }
@override void dispose() { nameController.dispose(); super.dispose(); }}Configuration du TextField
Section titled “Configuration du TextField”InputDecoration
Section titled “InputDecoration”L’InputDecoration permet de personnaliser l’apparence du champ :
TextField( controller: controller, decoration: InputDecoration( label: Text('Email'), hintText: 'Entrez votre email', prefixIcon: Icon(Icons.email), suffixIcon: Icon(Icons.clear), filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder(), errorText: hasError ? 'Email invalide' : null, ),)Types de clavier
Section titled “Types de clavier”La propriété keyboardType permet de configurer le clavier adapté :
TextField( keyboardType: TextInputType.emailAddress, // Clavier email)
TextField( keyboardType: TextInputType.phone, // Clavier numérique)
TextField( keyboardType: TextInputType.multiline, // Texte multiligne maxLines: null,)Types disponibles :
TextInputType.text- Clavier par défautTextInputType.emailAddress- Clavier emailTextInputType.phone- Clavier téléphoneTextInputType.number- Clavier numériqueTextInputType.multiline- Texte multiligneTextInputType.datetime- Saisie de date/heure
Écouteurs d’événements
Section titled “Écouteurs d’événements”TextField( onChanged: (value) => print('Texte modifié: $value'), onEditingComplete: () => print('Édition terminée'), onSubmitted: (value) => print('Formulaire soumis: $value'), onTap: () => print('Champ tapé'),)Formatage du texte
Section titled “Formatage du texte”Il est possible de formater automatiquement le texte saisi avec des TextInputFormatter :
class SpaceFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { if (newValue.text.isEmpty || newValue.text.length == 1) return newValue;
final formattedText = newValue.text.replaceAll(' ', '').split('').join(' '); return TextEditingValue( text: formattedText, selection: TextSelection.collapsed(offset: formattedText.length), ); }}
// UtilisationTextField( inputFormatters: [SpaceFormatter()],)Validation de formulaires
Section titled “Validation de formulaires”Form et FormState
Section titled “Form et FormState”Flutter propose le widget Form pour centraliser la gestion des formulaires. Il simplifie la validation, la réinitialisation et la sauvegarde.
class LoginForm extends StatefulWidget { @override _LoginFormState createState() => _LoginFormState();}
class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController();
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Email obligatoire'; } if (!value.contains('@')) { return 'Email invalide'; } return null; }, ), TextFormField( controller: _passwordController, decoration: InputDecoration(labelText: 'Mot de passe'), obscureText: true, validator: (value) { if (value == null || value.isEmpty) { return 'Mot de passe obligatoire'; } if (value.length < 6) { return 'Mot de passe trop court'; } return null; }, ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); // Traitement du formulaire print('Formulaire valide'); } }, child: Text('Connexion'), ), ], ), ); }}TextFormField
Section titled “TextFormField”TextFormField intègre directement les fonctionnalités de validation :
TextFormField( controller: fieldController, validator: (String? value) { if (value == null || value.isEmpty) { return 'Champ obligatoire'; } return null; }, onSaved: (String? value) { // Sauvegarder la valeur },)Validation personnalisée
Section titled “Validation personnalisée”String? validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email obligatoire'; }
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value)) { return 'Format d\'email invalide'; }
return null;}
String? validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Mot de passe obligatoire'; }
if (value.length < 8) { return 'Minimum 8 caractères'; }
if (!value.contains(RegExp(r'[A-Z]'))) { return 'Au moins une majuscule'; }
if (!value.contains(RegExp(r'[0-9]'))) { return 'Au moins un chiffre'; }
return null;}Widgets d’input
Section titled “Widgets d’input”Switch
Section titled “Switch”Le Switch permet de créer un interrupteur binaire :
class SwitchExample extends StatefulWidget { @override _SwitchExampleState createState() => _SwitchExampleState();}
class _SwitchExampleState extends State<SwitchExample> { bool switched = false;
@override Widget build(BuildContext context) { return Switch( value: switched, onChanged: (value) => setState(() => switched = value), ); }}Checkbox
Section titled “Checkbox”class CheckboxExample extends StatefulWidget { @override _CheckboxExampleState createState() => _CheckboxExampleState();}
class _CheckboxExampleState extends State<CheckboxExample> { bool checked = false;
@override Widget build(BuildContext context) { return Row( children: [ Checkbox( value: checked, onChanged: (value) => setState(() => checked = value!), ), InkWell( child: const Text("J'accepte les CGU !"), onTap: () => setState(() => checked = !checked), ), ], ); }}Radio buttons
Section titled “Radio buttons”Les boutons radio permettent de sélectionner une option parmi plusieurs :
class RadioExample extends StatefulWidget { @override _RadioExampleState createState() => _RadioExampleState();}
class _RadioExampleState extends State<RadioExample> { int radioSelection = 1;
@override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( children: [ Radio<int>( value: 1, groupValue: radioSelection, onChanged: (value) => setState(() => radioSelection = value!), ), const Text('Option 1') ], ), Row( children: [ Radio<int>( value: 2, groupValue: radioSelection, onChanged: (value) => setState(() => radioSelection = value!), ), const Text('Option 2') ], ), ], ); }}Slider
Section titled “Slider”Le Slider permet la sélection d’une valeur numérique :
class SliderExample extends StatefulWidget { @override _SliderExampleState createState() => _SliderExampleState();}
class _SliderExampleState extends State<SliderExample> { double sliderValue = 5.0;
@override Widget build(BuildContext context) { return Column( children: [ Text('Valeur: ${sliderValue.toStringAsFixed(1)}'), Slider( value: sliderValue, min: 0, max: 10, divisions: 100, label: '${sliderValue.toStringAsPrecision(3)}', onChanged: (value) => setState(() => sliderValue = value), ), ], ); }}DropdownButton
Section titled “DropdownButton”class DropdownExample extends StatefulWidget { @override _DropdownExampleState createState() => _DropdownExampleState();}
class _DropdownExampleState extends State<DropdownExample> { int? citySelection;
@override Widget build(BuildContext context) { return DropdownButton<int>( value: citySelection, hint: const Text('Sélectionnez votre destination'), items: const [ DropdownMenuItem(child: Text('Madrid'), value: 1), DropdownMenuItem(child: Text('Rome'), value: 2), DropdownMenuItem(child: Text('Lisbonne'), value: 3), ], onChanged: (int? value) => setState(() => citySelection = value), ); }}AutoComplete
Section titled “AutoComplete”Le widget Autocomplete intègre un mécanisme de suggestion automatique :
const cities = ['Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice'];
class AutoCompleteExample extends StatefulWidget { @override _AutoCompleteExampleState createState() => _AutoCompleteExampleState();}
class _AutoCompleteExampleState extends State<AutoCompleteExample> { String? selectedCity;
@override Widget build(BuildContext context) { return Column( children: [ Autocomplete<String>( optionsBuilder: (TextEditingValue value) { if (value.text.isEmpty) return <String>[]; return cities.where((element) => element.toLowerCase().contains(value.text.toLowerCase()) ); }, onSelected: (value) => setState(() => selectedCity = value), fieldViewBuilder: (context, controller, focusNode, onEditingComplete) { return TextFormField( controller: controller, focusNode: focusNode, onEditingComplete: onEditingComplete, decoration: InputDecoration( labelText: 'Ville', border: OutlineInputBorder(), ), ); }, ), if (selectedCity != null) Padding( padding: const EdgeInsets.all(8.0), child: Text('Ville sélectionnée: $selectedCity'), ), ], ); }}Création de FormField personnalisés
Section titled “Création de FormField personnalisés”FormField générique
Section titled “FormField générique”FormField permet de créer des éléments de formulaires personnalisés :
class SliderField extends FormField<double> { SliderField({ Key? key, FormFieldValidator<double>? validator, double initialValue = 0, }) : super( key: key, initialValue: initialValue, validator: validator, builder: (FormFieldState<double> state) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Text('Valeur: ${state.value?.toStringAsFixed(1) ?? '0'}'), Slider( value: state.value ?? 0, min: 0, max: 10, onChanged: (value) => state.didChange(value), ), if (state.hasError) Text( state.errorText!, style: const TextStyle(color: Colors.red), ) ], ), ); }, );}FormField avec Radio buttons
Section titled “FormField avec Radio buttons”class ConfortField extends FormField<int> { ConfortField({Key? key}) : super( key: key, initialValue: null, builder: (FormFieldState<int> state) => Column( mainAxisSize: MainAxisSize.min, children: [ if (state.hasError) Text( state.errorText!, style: const TextStyle(color: Colors.red), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( children: [ Radio<int>( value: 1, groupValue: state.value, onChanged: (value) => state.didChange(value), ), const Text('2nde classe') ], ), Row( children: [ Radio<int>( value: 2, groupValue: state.value, onChanged: (value) => state.didChange(value), ), const Text('1ère classe') ], ), ], ), ], ), validator: (value) => value != null ? null : 'Choix obligatoire', );}Utilisation des FormField personnalisés
Section titled “Utilisation des FormField personnalisés”class CustomFormExample extends StatelessWidget { final _formKey = GlobalKey<FormState>();
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ SliderField( validator: (value) { if (value == null || value < 5) { return 'La valeur doit être d\'au moins 5'; } return null; }, ), ConfortField(), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { print('Formulaire valide'); } }, child: Text('Valider'), ), ], ), ); }}Bonnes pratiques pour les formulaires
Section titled “Bonnes pratiques pour les formulaires”1. Gestion de la mémoire
Section titled “1. Gestion de la mémoire”Toujours disposer des contrôleurs pour éviter les fuites mémoire :
class FormScreen extends StatefulWidget { @override _FormScreenState createState() => _FormScreenState();}
class _FormScreenState extends State<FormScreen> { final _emailController = TextEditingController(); final _passwordController = TextEditingController();
@override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { // Formulaire... }}2. Validation en temps réel
Section titled “2. Validation en temps réel”Implémenter une validation progressive pour une meilleure UX :
class ProgressiveValidationForm extends StatefulWidget { @override _ProgressiveValidationFormState createState() => _ProgressiveValidationFormState();}
class _ProgressiveValidationFormState extends State<ProgressiveValidationForm> { final _formKey = GlobalKey<FormState>(); bool _autoValidate = false;
@override Widget build(BuildContext context) { return Form( key: _formKey, autovalidateMode: _autoValidate ? AutovalidateMode.onUserInteraction : AutovalidateMode.disabled, child: Column( children: [ TextFormField( decoration: InputDecoration(labelText: 'Email'), validator: validateEmail, ), ElevatedButton( onPressed: () { setState(() => _autoValidate = true); if (_formKey.currentState!.validate()) { // Traitement... } }, child: Text('Valider'), ), ], ), ); }}3. États de chargement
Section titled “3. États de chargement”Gérer les états de chargement lors de la soumission :
class AsyncFormExample extends StatefulWidget { @override _AsyncFormExampleState createState() => _AsyncFormExampleState();}
class _AsyncFormExampleState extends State<AsyncFormExample> { final _formKey = GlobalKey<FormState>(); bool _isLoading = false;
Future<void> _submitForm() async { if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try { // Simulation d'une requête réseau await Future.delayed(Duration(seconds: 2));
ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Formulaire envoyé avec succès')), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur: $e')), ); } finally { setState(() => _isLoading = false); } }
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( decoration: InputDecoration(labelText: 'Nom'), validator: (value) => value?.isEmpty == true ? 'Obligatoire' : null, ), SizedBox(height: 20), ElevatedButton( onPressed: _isLoading ? null : _submitForm, child: _isLoading ? CircularProgressIndicator(color: Colors.white) : Text('Envoyer'), ), ], ), ); }}4. Accessibilité
Section titled “4. Accessibilité”Améliorer l’accessibilité avec des labels appropriés :
TextFormField( decoration: InputDecoration( labelText: 'Email', helperText: 'Votre adresse email professionnelle', ), keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: validateEmail,)5. Thématisation
Section titled “5. Thématisation”Personnaliser l’apparence des formulaires via le thème :
MaterialApp( theme: ThemeData( inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder(), filled: true, fillColor: Colors.grey[100], ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( minimumSize: Size(double.infinity, 48), ), ), ), home: MyFormScreen(),)6. Gestion des erreurs réseau
Section titled “6. Gestion des erreurs réseau”Implémenter une gestion robuste des erreurs :
class ErrorHandlingForm extends StatefulWidget { @override _ErrorHandlingFormState createState() => _ErrorHandlingFormState();}
class _ErrorHandlingFormState extends State<ErrorHandlingForm> { final _formKey = GlobalKey<FormState>(); String? _globalError;
Future<void> _submitForm() async { setState(() => _globalError = null);
if (!_formKey.currentState!.validate()) return;
try { // Soumission du formulaire await submitToServer(); } on NetworkException catch (e) { setState(() => _globalError = 'Erreur réseau: Vérifiez votre connexion'); } on ValidationException catch (e) { setState(() => _globalError = 'Données invalides: ${e.message}'); } catch (e) { setState(() => _globalError = 'Erreur inattendue: $e'); } }
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ if (_globalError != null) Container( padding: EdgeInsets.all(12), color: Colors.red[100], child: Text( _globalError!, style: TextStyle(color: Colors.red[800]), ), ), // Autres champs... ], ), ); }}7. Sauvegarde automatique
Section titled “7. Sauvegarde automatique”Implémenter une sauvegarde automatique des données :
class AutoSaveForm extends StatefulWidget { @override _AutoSaveFormState createState() => _AutoSaveFormState();}
class _AutoSaveFormState extends State<AutoSaveForm> { final _emailController = TextEditingController(); Timer? _debounceTimer;
@override void initState() { super.initState(); _emailController.addListener(_onTextChanged); _loadSavedData(); }
void _onTextChanged() { _debounceTimer?.cancel(); _debounceTimer = Timer(Duration(milliseconds: 500), () { _saveData(); }); }
void _saveData() { // Sauvegarder localement SharedPreferences.getInstance().then((prefs) { prefs.setString('draft_email', _emailController.text); }); }
void _loadSavedData() { SharedPreferences.getInstance().then((prefs) { final savedEmail = prefs.getString('draft_email'); if (savedEmail != null) { _emailController.text = savedEmail; } }); }
@override Widget build(BuildContext context) { return TextField( controller: _emailController, decoration: InputDecoration( labelText: 'Email', suffixIcon: Icon(Icons.cloud_done, color: Colors.green), ), ); }
@override void dispose() { _debounceTimer?.cancel(); _emailController.dispose(); super.dispose(); }}Ces bonnes pratiques garantissent des formulaires robustes, accessibles et offrant une excellente expérience utilisateur dans vos applications Flutter.