Exercice 1 : Création d'un formulaire avec validation
Objectif : Créer un formulaire avec deux champs (Nom et Email) et valider les données avant
de soumettre le formulaire.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. Le widget MyApp sera exécuté au
démarrage.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// MaterialApp est le widget racine de l'application, qui contient
toute la structure de l'UI.
home: Scaffold(
appBar: AppBar(
title: Text('Formulaire avec validation'),
// AppBar est la barre d'application en haut, ici on y met le
titre 'Formulaire avec validation'.
),
body: Padding(
padding: const EdgeInsets.all(16.0),
// Padding autour du formulaire pour lui donner un peu d'espace.
child: Formulaire(),
// Affiche le widget Formulaire qui est un StatefulWidget.
),
),
);
1
}
class Formulaire extends StatefulWidget {
@override
_FormulaireState createState() => _FormulaireState();
// Cette méthode crée l'état du formulaire, nécessaire pour gérer l'état
interne du widget (données saisies, validation, etc.).
class _FormulaireState extends State<Formulaire> {
final _formKey = GlobalKey<FormState>();
// _formKey est une clé globale pour le formulaire. Elle permet de
valider et de gérer l'état du formulaire.
final TextEditingController _nomController = TextEditingController();
// _nomController est un contrôleur pour le champ 'Nom'. Il permet de
récupérer et de manipuler le texte saisi.
final TextEditingController _emailController = TextEditingController();
// _emailController est un contrôleur pour le champ 'Email'. Il permet de
récupérer et de manipuler le texte saisi.
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
// Attache la clé globale (_formKey) au formulaire, permettant de
valider et de soumettre le formulaire.
child: Column(
2
// Le formulaire contient une colonne pour disposer les champs du
formulaire verticalement.
crossAxisAlignment: CrossAxisAlignment.start,
// Aligne les champs à gauche.
children: <Widget>[
TextFormField(
controller: _nomController,
// Lier le contrôleur au champ de texte pour pouvoir récupérer
les données saisies.
decoration: InputDecoration(labelText: 'Nom'),
// Décoration du champ de texte, ici le label du champ est
'Nom'.
validator: (value) {
// Fonction de validation pour vérifier si le champ est
correctement rempli.
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom';
// Si le champ est vide, on renvoie un message d'erreur.
return null;
// Si le champ est valide (non vide), on retourne null pour
indiquer que la validation est réussie.
},
),
TextFormField(
controller: _emailController,
// Lier le contrôleur au champ de texte pour pouvoir récupérer
les données saisies.
decoration: InputDecoration(labelText: 'Email'),
// Décoration du champ de texte, ici le label du champ est
'Email'.
3
validator: (value) {
// Fonction de validation pour vérifier si l'email est bien
renseigné et valide.
if (value == null || value.isEmpty) {
return 'Veuillez entrer un email';
// Si le champ est vide, on renvoie un message d'erreur.
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
// Si l'email ne correspond pas à une expression régulière
valide (pas de @ ou de point manquants), on renvoie une erreur.
return 'Email invalide';
return null;
// Si l'email est valide, on retourne null pour indiquer que
la validation est réussie.
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
// Padding pour ajouter de l'espace autour du bouton
'Soumettre'.
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// On vérifie si le formulaire est valide (tous les
champs sont remplis correctement).
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Formulaire valide'))
// Si la validation réussit, on affiche un message de
confirmation sous forme de SnackBar.
);
4
}
},
child: Text('Soumettre'),
// Texte affiché sur le bouton 'Soumettre'.
),
),
],
),
);
Explication :
GlobalKey<FormState> _formKey : Permet de gérer l'état du formulaire et de valider
les champs.
TextFormField : Un champ de texte avec une fonction de validation.
RegExp : Expression régulière pour valider l'email.
ScaffoldMessenger.of(context).showSnackBar() : Affiche un message
contextuel si le formulaire est valide.
Exercice 2 : Navigation avec passage d'arguments
Objectif : Créer une application avec deux pages, où la première page passe un argument à la
deuxième page via la navigation.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Lancer l'application Flutter. Cela appelle la méthode runApp pour
afficher l'interface utilisateur définie par MyApp.
class MyApp extends StatelessWidget {
@override
5
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
// Définit la route initiale lorsque l'application démarre. Ici,
c'est la page d'accueil (route '/').
routes: {
'/': (context) => HomePage(),
// La route '/' est liée à la page d'accueil (HomePage).
'/second': (context) => SecondPage(),
// La route '/second' est liée à la page secondaire (SecondPage).
},
);
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page d\'accueil')),
// Création de l'AppBar (barre de titre) de la page d'accueil avec le
texte "Page d'accueil".
body: Center(
child: ElevatedButton(
onPressed: () {
6
// Lors du clic sur le bouton, la méthode onPressed est
appelée.
Navigator.pushNamed(
context,
'/second',
arguments: 'Bonjour depuis la page d\'accueil',
// Utilise la méthode pushNamed pour naviguer vers la page
secondaire.
// Elle passe aussi un argument ('Bonjour depuis la page
d\'accueil') à la page secondaire.
);
},
child: Text('Aller à la page secondaire'),
// Le texte affiché sur le bouton : "Aller à la page secondaire".
),
),
);
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final String message = ModalRoute.of(context)!.settings.arguments as
String;
// Récupération de l'argument passé lors de la navigation avec
Navigator.pushNamed.
// ModalRoute.of(context) obtient les paramètres de la route actuelle,
y compris les arguments.
// Ici, on s'assure que l'argument est de type String.
7
return Scaffold(
appBar: AppBar(title: Text('Page secondaire')),
// Création de l'AppBar pour la page secondaire avec le texte "Page
secondaire".
body: Center(
child: Text(message),
// Affiche le message récupéré de la page précédente.
// Ce message est passé en argument via la méthode pushNamed.
),
);
Explication :
Navigator.pushNamed() : Permet de naviguer vers une nouvelle route avec des
arguments.
ModalRoute.of(context)!.settings.arguments : Récupère les arguments passés
à la route.
Exercice 3 : Utilisation de StreamBuilder pour afficher une liste dynamique
Objectif : Créer une interface avec un StreamBuilder qui affiche des données en temps réel.
import 'dart:async';
// Importation de la bibliothèque dart:async qui fournit les classes pour
travailler avec des streams.
import 'package:flutter/material.dart';
// Importation de la bibliothèque Flutter Material pour l'UI.
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. Cela appelle la méthode runApp
pour afficher l'interface utilisateur définie par MyApp.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: StreamExample());
// MaterialApp est l'élément de base pour l'application. Il affiche le
widget StreamExample comme page d'accueil.
8
}
}
class StreamExample extends StatelessWidget {
final StreamController<String> _controller = StreamController<String>();
// Création d'un contrôleur de stream qui permettra d'ajouter des
éléments à un stream.
// Ce contrôleur est de type String, ce qui signifie que les éléments
ajoutés au stream seront des chaînes de caractères.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('StreamBuilder Exemple')),
// Scaffold est un widget qui fournit une structure de base avec une
AppBar et un body.
// L'AppBar affiche le titre "StreamBuilder Exemple" en haut de la
page.
body: Column(
// Utilisation de Column pour disposer les widgets (un bouton et un
StreamBuilder) verticalement.
children: [
ElevatedButton(
onPressed: () {
_controller.add('Nouvel élément ${DateTime.now()}');
// Lors du clic sur le bouton, un nouvel élément est ajouté
au stream.
// L'élément ajouté est une chaîne de caractères comprenant
la date et l'heure actuelles.
},
child: Text('Ajouter un élément'),
// Texte affiché sur le bouton : "Ajouter un élément".
),
StreamBuilder<String>(
stream: _controller.stream,
// Le widget StreamBuilder écoute le stream du contrôleur
_controller.
// Il réagit aux événements qui arrivent dans le stream.
builder: (context, snapshot) {
// Le builder fournit le contexte et le snapshot, qui
contient les données ou l'état du stream.
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
// Si le stream est en attente de données (connectionState
est "waiting"),
// on affiche un indicateur de chargement circulaire.
} else if (snapshot.hasError) {
return Text('Erreur: ${snapshot.error}');
// Si une erreur est survenue lors de l'écoute du stream,
on affiche le message d'erreur.
} else if (!snapshot.hasData) {
return Text('Aucun élément');
// Si le stream ne contient aucune donnée, on affiche
"Aucun élément".
} else {
return Text(snapshot.data!);
// Si le stream contient des données, on les affiche sous
forme de texte.
9
// "snapshot.data!" est une chaîne de caractères provenant
du stream.
}
},
),
],
),
);
}
}
Explication :
StreamController<String> : Permet de gérer et émettre des données à un stream.
StreamBuilder<String> : Reconstruit l'UI chaque fois que le stream émet de
nouvelles données.
snapshot.connectionState : Permet de gérer l'état du stream (en attente, succès,
erreur).
Exercice 4 : Utilisation d'un FutureBuilder pour effectuer une requête API
Objectif : Créer une interface qui fait une requête HTTP et affiche la réponse avec un
FutureBuilder.
import 'dart:convert';
// Importation de la bibliothèque 'dart:convert' qui permet de décoder des
données JSON.
import 'package:flutter/material.dart';
// Importation de la bibliothèque Flutter Material pour l'UI.
import 'package:http/http.dart' as http;
// Importation de la bibliothèque HTTP pour effectuer des requêtes réseau
(comme GET).
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. Cette fonction démarre
l'application en appelant la méthode runApp avec MyApp.
class MyApp extends StatelessWidget {
10
@override
Widget build(BuildContext context) {
return MaterialApp(home: ApiExample());
// MaterialApp est l'élément de base pour l'application. Il affiche le
widget ApiExample comme page d'accueil.
class ApiExample extends StatelessWidget {
// Fonction asynchrone qui récupère des données via une requête HTTP GET.
Future<String> fetchData() async {
// On effectue une requête GET sur une API (ici,
jsonplaceholder.typicode.com) pour récupérer un post.
final response = await
http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
if (response.statusCode == 200) {
// Si le code de statut HTTP est 200 (succès), on décode la réponse
JSON.
var data = jsonDecode(response.body);
// Le corps de la réponse est décodé en objet Dart (en un Map).
return data['title'];
// On retourne le titre du post (la clé 'title' dans la réponse
JSON).
} else {
throw Exception('Échec de la récupération des données');
// Si le code de statut n'est pas 200, on lance une exception pour
signaler l'erreur.
11
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FutureBuilder Exemple')),
// Création de l'AppBar de l'application avec le titre "FutureBuilder
Exemple".
body: Center(
child: FutureBuilder<String>(
// FutureBuilder permet de travailler avec des données récupérées
de façon asynchrone (ici, depuis l'API).
future: fetchData(),
// On passe la fonction future 'fetchData' à FutureBuilder, qui
va exécuter la requête HTTP.
builder: (context, snapshot) {
// Le builder reçoit le contexte et le snapshot (qui contient
l'état de la requête et les données).
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
// Si la connexion est en attente (le future est encore en
cours d'exécution), on affiche un indicateur de chargement.
} else if (snapshot.hasError) {
return Text('Erreur: ${snapshot.error}');
// Si une erreur se produit, on affiche le message d'erreur
dans l'UI.
} else {
return Text('Titre: ${snapshot.data}');
// Si les données sont récupérées avec succès, on affiche le
titre du post récupéré.
12
}
},
),
),
);
Explication :
Future<String> fetchData() : Fonction qui effectue une requête HTTP
asynchrone.
FutureBuilder<String> : Permet d'attendre et d'afficher un widget en fonction de
l'état du Future.
Exercice 5 : Utilisation d'un InheritedWidget pour partager des données
Objectif : Utiliser un InheritedWidget pour partager des données entre différents widgets
de l'application.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. Appelle la méthode runApp pour
afficher l'interface utilisateur définie par MyApp.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: InheritedWidgetExample());
// MaterialApp est l'élément de base pour l'application. Il affiche le
widget InheritedWidgetExample comme page d'accueil.
13
class Counter extends InheritedWidget {
final int count; // Déclare la variable count pour stocker la valeur du
compteur.
final Widget child; // Le widget enfant qui peut accéder à l'état de
l'inherited widget.
Counter({required this.count, required this.child}) : super(child:
child);
// Constructeur de Counter, qui prend un int count et un Widget child.
// Le widget child est transmis au constructeur de la classe parente
(InheritedWidget).
@override
bool updateShouldNotify(Counter oldWidget) {
// Cette méthode est appelée pour vérifier si l'état a changé.
// Si l'état a changé (ici, la valeur de 'count'), on retourne true
pour notifier les widgets enfants.
return oldWidget.count != count;
static Counter? of(BuildContext context) {
// Méthode statique qui permet de récupérer l'instance de Counter dans
le widget contextuel actuel.
return context.dependOnInheritedWidgetOfExactType<Counter>();
// dependOnInheritedWidgetOfExactType permet de récupérer un widget
InheritedWidget par son type exact.
class InheritedWidgetExample extends StatelessWidget {
14
@override
Widget build(BuildContext context) {
return Counter(
count: 5,
// On crée un Counter avec la valeur du compteur égale à 5 et le
widget enfant défini en dessous.
child: Scaffold(
appBar: AppBar(title: Text('InheritedWidget Exemple')),
// Création d'un AppBar avec le titre "InheritedWidget Exemple".
body: Center(
child: ElevatedButton(
onPressed: () {
final counter = Counter.of(context);
// Récupération de l'instance de Counter via la méthode
statique 'of', en utilisant le BuildContext.
print('Valeur du compteur: ${counter?.count}');
// Affichage de la valeur du compteur dans la console, si
Counter est trouvé.
},
child: Text('Afficher la valeur du compteur'),
// Texte du bouton : "Afficher la valeur du compteur".
),
),
),
);
Explication :
15
InheritedWidget : Permet de partager un état (ici, un compteur) dans l'arbre des
widgets.
updateShouldNotify : Vérifie si l'état du widget a changé.
Counter.of(context) : Permet d'accéder à l'état dans un widget descendant.
Exercice 6 : Animation avec AnimatedContainer
Objectif : Animer la taille et la couleur d'un Container avec un AnimatedContainer.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. Lance l'application en appelant
la méthode runApp et affiche MyApp.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: AnimatedContainerExample());
// MaterialApp est utilisé pour créer une application avec un thème
material design.
// Ici, la page d'accueil de l'application est
AnimatedContainerExample, qui montre un exemple d'animation.
}
}
class AnimatedContainerExample extends StatefulWidget {
@override
_AnimatedContainerExampleState createState() =>
_AnimatedContainerExampleState();
// Création d'un widget StatefulWidget pour pouvoir manipuler l'état du
container (sa taille et sa couleur).
}
class _AnimatedContainerExampleState extends
State<AnimatedContainerExample> {
// Déclaration des variables d'état pour la largeur, la hauteur et la
couleur du container.
double _width = 100;
double _height = 100;
Color _color = Colors.blue;
@override
void initState() {
super.initState();
// initState est appelé lors de l'initialisation de l'état du widget,
ici pour modifier l'état après 2 secondes.
Future.delayed(Duration(seconds: 2), () {
setState(() {
// Après 2 secondes, la méthode setState est appelée pour modifier
les valeurs d'état.
_width = 200; // Modification de la largeur du container
_height = 200; // Modification de la hauteur du container
_color = Colors.red; // Modification de la couleur du container
});
16
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('AnimatedContainer Exemple')),
// Création de la barre d'application avec un titre
"AnimatedContainer Exemple".
body: Center(
child: AnimatedContainer(
width: _width, // Largeur animée. L'animation ajustera cette
largeur à partir de sa valeur initiale.
height: _height, // Hauteur animée. L'animation ajustera cette
hauteur également.
color: _color, // Couleur animée. La couleur du container
changera lors de l'animation.
duration: Duration(seconds: 2),
// Durée de l'animation. L'animation prendra 2 secondes pour se
terminer après que l'état soit modifié.
curve: Curves.easeInOut,
// Courbe d'animation. `easeInOut` permet à l'animation de
démarrer lentement, puis d'accélérer et de ralentir avant la fin.
),
),
);
}
}
Explication :
AnimatedContainer : Permet d'animer les propriétés du container, comme la taille et
la couleur.
setState() : Déclenche la reconstruction de l'UI avec les nouvelles valeurs.
duration et curve : Spécifient la durée et le type d'animation.
Exercice 7 : Utilisation de ListView.builder
Objectif : Créer une liste dynamique avec un ListView.builder qui affiche des éléments
générés.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. La méthode runApp lance
l'application et affiche MyApp.
17
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ListViewExample());
// MaterialApp est le widget de base qui applique le thème Material
Design à l'application.
// Ici, il affiche la page ListViewExample comme écran d'accueil.
class ListViewExample extends StatelessWidget {
final List<String> items = List.generate(20, (index) => 'Élément ${index
+ 1}');
// Génération d'une liste dynamique contenant 20 éléments. Chaque élément
est une chaîne de caractères "Élément 1", "Élément 2", etc.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ListView.builder Exemple')),
// Création de l'AppBar avec le titre "ListView.builder Exemple".
body: ListView.builder(
itemCount: items.length,
// `itemCount` définit le nombre d'éléments dans la liste. Ici, il
est égal à la longueur de la liste `items`.
itemBuilder: (context, index) {
// `itemBuilder` est une fonction qui permet de construire chaque
élément de la liste. Elle prend l'index de l'élément actuel.
18
return ListTile(
title: Text(items[index]),
// Chaque élément de la liste est un `ListTile`, avec le texte
affichant l'élément à l'index donné dans la liste `items`.
);
},
),
);
Explication :
ListView.builder : Crée une liste paresseuse qui ne construit que les éléments
visibles.
itemCount : Nombre d'éléments dans la liste.
itemBuilder : Fonction qui construit chaque élément de la liste.
Exercice 8 : Gestion d'état avec Provider
Objectif : Utiliser le package provider pour gérer l'état et le partager entre plusieurs widgets.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // Importation de la bibliothèque
Provider pour la gestion d'état.
void main() => runApp(MyApp());
// Point d'entrée de l'application Flutter. La méthode runApp lance
l'application et affiche MyApp.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
19
create: (_) => Counter(), // Création et fourniture d'une instance de
Counter, un modèle de gestion d'état.
child: MaterialApp(home: CounterPage()),
// MaterialApp permet d'appliquer un thème Material à l'application.
La page d'accueil est CounterPage.
);
class Counter extends ChangeNotifier {
int _count = 0; // Variable privée _count pour stocker la valeur du
compteur
int get count => _count;
// Getter pour récupérer la valeur actuelle du compteur (lecture de
_count).
void increment() {
_count++; // Incrémenter la valeur du compteur de 1
notifyListeners();
// Notifier tous les écouteurs que l'état du compteur a changé.
// Cela permet de redessiner tous les widgets qui écoutent ce modèle.
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
20
// Utilisation de Provider pour récupérer l'instance actuelle de
Counter.
// Provider.of<Counter>(context) permet d'accéder à l'état du compteur
qui a été fourni par ChangeNotifierProvider.
// Cette instance est automatiquement mise à jour lorsque l'état change
(via notifyListeners).
return Scaffold(
appBar: AppBar(title: Text('Provider Exemple')),
// AppBar avec un titre "Provider Exemple".
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// Centre les widgets enfants dans l'axe principal (vertical).
children: [
Text('Compteur: ${counter.count}'),
// Affichage de la valeur actuelle du compteur en récupérant
counter.count.
ElevatedButton(
onPressed: () {
counter.increment(); // Lorsque le bouton est pressé, le
compteur est incrémenté.
},
child: Text('Incrémenter'),
// Affichage du texte du bouton "Incrémenter".
),
],
21
),
),
);
Explication :
ChangeNotifierProvider : Permet de fournir une instance de modèle (Counter) à
l'arbre des widgets.
notifyListeners() : Avertit les widgets abonnés que l'état a changé.
Provider.of<Counter>(context) : Récupère l'état dans l'arbre des widgets pour
l'afficher.
22