Skip to content

Formulaires et saisie de données 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.

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();
}
}

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,
),
)

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éfaut
  • TextInputType.emailAddress - Clavier email
  • TextInputType.phone - Clavier téléphone
  • TextInputType.number - Clavier numérique
  • TextInputType.multiline - Texte multiligne
  • TextInputType.datetime - Saisie de date/heure
TextField(
onChanged: (value) => print('Texte modifié: $value'),
onEditingComplete: () => print('Édition terminée'),
onSubmitted: (value) => print('Formulaire soumis: $value'),
onTap: () => print('Champ tapé'),
)

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),
);
}
}
// Utilisation
TextField(
inputFormatters: [SpaceFormatter()],
)

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 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
},
)
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;
}

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),
);
}
}
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),
),
],
);
}
}

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')
],
),
],
);
}
}

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),
),
],
);
}
}
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),
);
}
}

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'),
),
],
);
}
}

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),
)
],
),
);
},
);
}
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',
);
}
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'),
),
],
),
);
}
}

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...
}
}

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'),
),
],
),
);
}
}

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'),
),
],
),
);
}
}

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,
)

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(),
)

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...
],
),
);
}
}

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.