Skip to content

Création de FormField personnalisés Flutter - SliderField et ConfortField

FormField permet de créer des éléments de formulaires personnalisés qui s’intègrent parfaitement au système de validation de Flutter. Ces widgets personnalisés peuvent gérer leur propre état et validation.

Voici la structure de base d’un FormField personnalisé :

class CustomFormField<T> extends FormField<T> {
CustomFormField({
Key? key,
FormFieldValidator<T>? validator,
T? initialValue,
AutovalidateMode? autovalidateMode,
}) : super(
key: key,
initialValue: initialValue,
validator: validator,
autovalidateMode: autovalidateMode,
builder: (FormFieldState<T> state) {
return Column(
children: [
// Votre widget personnalisé ici
if (state.hasError)
Text(
state.errorText!,
style: TextStyle(color: Colors.red),
),
],
);
},
);
}

Voici un exemple complet d’un SliderField intégré au système de validation :

class SliderField extends FormField<double> {
SliderField({
Key? key,
FormFieldValidator<double>? validator,
double initialValue = 0,
double min = 0,
double max = 10,
int? divisions,
String? labelPrefix,
}) : super(
key: key,
initialValue: initialValue,
validator: validator,
builder: (FormFieldState<double> state) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${labelPrefix ?? "Valeur"}: ${state.value?.toStringAsFixed(1) ?? "0"}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Slider(
value: state.value ?? initialValue,
min: min,
max: max,
divisions: divisions,
label: state.value?.toStringAsFixed(1),
onChanged: (value) => state.didChange(value),
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
state.errorText!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
),
);
},
);
}

ConfortField - Boutons radio avec validation

Section titled “ConfortField - Boutons radio avec validation”

Exemple d’un FormField avec des boutons radio :

class ConfortField extends FormField<int> {
ConfortField({
Key? key,
FormFieldValidator<int>? validator,
int? initialValue,
}) : super(
key: key,
initialValue: initialValue,
validator: validator ?? (value) => value != null ? null : 'Choix obligatoire',
builder: (FormFieldState<int> state) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Classe de confort',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
state.errorText!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
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')
],
),
],
),
],
),
);
}

Un FormField pour la sélection de dates :

class DatePickerField extends FormField<DateTime> {
DatePickerField({
Key? key,
FormFieldValidator<DateTime>? validator,
DateTime? initialValue,
String? labelText,
}) : super(
key: key,
initialValue: initialValue,
validator: validator,
builder: (FormFieldState<DateTime> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () async {
final date = await showDatePicker(
context: state.context,
initialDate: state.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime(2100),
);
if (date != null) {
state.didChange(date);
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: labelText ?? 'Date',
border: OutlineInputBorder(),
suffixIcon: Icon(Icons.calendar_today),
errorText: state.hasError ? state.errorText : null,
),
child: Text(
state.value != null
? DateFormat('dd/MM/yyyy').format(state.value!)
: 'Sélectionner une date',
style: TextStyle(
color: state.value != null
? Theme.of(state.context).textTheme.bodyLarge?.color
: Theme.of(state.context).hintColor,
),
),
),
),
],
);
},
);
}

Un FormField pour la sélection de tags multiples :

class TagsField extends FormField<List<String>> {
TagsField({
Key? key,
FormFieldValidator<List<String>>? validator,
List<String> initialValue = const [],
required List<String> availableTags,
String? labelText,
}) : super(
key: key,
initialValue: initialValue,
validator: validator,
builder: (FormFieldState<List<String>> state) {
final selectedTags = state.value ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (labelText != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
labelText,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: availableTags.map((tag) {
final isSelected = selectedTags.contains(tag);
return FilterChip(
label: Text(tag),
selected: isSelected,
onSelected: (selected) {
final newTags = List<String>.from(selectedTags);
if (selected) {
newTags.add(tag);
} else {
newTags.remove(tag);
}
state.didChange(newTags);
},
);
}).toList(),
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
state.errorText!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
);
},
);
}

Voici comment utiliser ces widgets personnalisés dans un formulaire :

class CustomFormExample extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Formulaire personnalisé')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
SliderField(
labelPrefix: 'Budget',
min: 0,
max: 1000,
divisions: 20,
validator: (value) {
if (value == null || value < 100) {
return 'Le budget doit être d\'au moins 100€';
}
return null;
},
),
SizedBox(height: 20),
ConfortField(
validator: (value) => value == null ? 'Sélection obligatoire' : null,
),
SizedBox(height: 20),
DatePickerField(
labelText: 'Date de départ',
validator: (value) {
if (value == null) {
return 'Date obligatoire';
}
if (value.isBefore(DateTime.now())) {
return 'La date doit être dans le futur';
}
return null;
},
),
SizedBox(height: 20),
TagsField(
labelText: 'Centres d\'intérêt',
availableTags: ['Sport', 'Culture', 'Nature', 'Gastronomie', 'Histoire'],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Sélectionnez au moins un centre d\'intérêt';
}
return null;
},
),
SizedBox(height: 30),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Formulaire valide !')),
);
}
},
child: Text('Valider'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48),
),
),
],
),
),
),
);
}
}

Bonnes pratiques pour les FormField personnalisés

Section titled “Bonnes pratiques pour les FormField personnalisés”

Utilisez state.didChange() pour notifier les changements :

onChanged: (value) => state.didChange(value),

Implémentez une validation cohérente avec les autres widgets :

validator: (value) {
if (value == null) return 'Champ obligatoire';
// Validation spécifique...
return null;
},

Ajoutez des propriétés d’accessibilité :

Semantics(
label: 'Sélecteur de budget',
value: '${state.value} euros',
child: Slider(
value: state.value ?? 0,
onChanged: (value) => state.didChange(value),
),
)

Respectez le thème de l’application :

Text(
state.errorText!,
style: TextStyle(
color: Theme.of(context).errorColor,
fontSize: 12,
),
)

Ces FormField personnalisés offrent une flexibilité maximale tout en conservant l’intégration native avec le système de validation de Flutter.