5.4Connexion aux Web Services et API REST
5.4.1 â Principes des API REST
đ Qu'est-ce qu'une API REST ?
Une API REST (Representational State Transfer) est un moyen de communiquer avec un serveur distant pour récupérer, créer, modifier ou supprimer des données. C'est comme un menu dans un restaurant : vous commandez ce que vous voulez, et le serveur vous apporte ce que vous avez demandé.
Imaginez que vous ĂȘtes dans une bibliothĂšque :
- GET : Vous demandez un livre (récupérer des données)
- POST : Vous ajoutez un nouveau livre (créer des données)
- PUT : Vous modifiez un livre existant (mettre à jour des données)
- DELETE : Vous retirez un livre (supprimer des données)
đĄ Les mĂ©thodes HTTP
Les API REST utilisent des méthodes HTTP standard pour indiquer l'action à effectuer :
đ Les URLs (endpoints)
Chaque API a des URLs spĂ©cifiques appelĂ©es "endpoints" qui indiquent oĂč trouver ou modifier les donnĂ©es :
// Exemples d'endpoints typiques
String baseUrl = 'https://api.exemple.com';
// Récupérer tous les utilisateurs
String getUsers = '$baseUrl/users'; // GET
// Récupérer un utilisateur spécifique
String getUser = '$baseUrl/users/123'; // GET
// Créer un nouvel utilisateur
String createUser = '$baseUrl/users'; // POST
// Modifier un utilisateur
String updateUser = '$baseUrl/users/123'; // PUT
// Supprimer un utilisateur
String deleteUser = '$baseUrl/users/123'; // DELETE
đŠ Format JSON
Les API REST communiquent généralement en JSON (JavaScript Object Notation), un format de données simple et lisible :
1. Objet JSON simple
Un objet JSON représente une entité avec plusieurs propriétés :
{
"id": 1,
"nom": "Jean Dupont",
"email": "jean@example.com",
"age": 30
}
2. Tableau d'objets JSON
Un tableau JSON contient plusieurs objets, souvent utilisé pour représenter une liste d'éléments. Voici un exemple réel retourné par l'API https://mazoul.online/json/listusers :
[
{
"id": 1,
"nom": "Alami",
"prenom": "Hassan",
"email": "hassan.alami@email.ma",
"telephone": "+212 6 12 34 56 78",
"ville": "Casablanca",
"dateInscription": "2024-01-15"
},
{
"id": 2,
"nom": "Bennani",
"prenom": "Fatima",
"email": "fatima.bennani@email.ma",
"telephone": "+212 6 23 45 67 89",
"ville": "Rabat",
"dateInscription": "2024-02-20"
},
{
"id": 3,
"nom": "El Idrissi",
"prenom": "Mohammed",
"email": "mohammed.idrissi@email.ma",
"telephone": "+212 6 34 56 78 90",
"ville": "Marrakech",
"dateInscription": "2024-03-10"
}
]
En Dart, JSON est représenté comme :
- Objet JSON â
Map<String, dynamic> - Tableau JSON â
List<dynamic>ouList<Map<String, dynamic>>
json.decode() et json.encode().
â Avantages des API REST
- Standardisé : Utilise des méthodes HTTP standard que tout le monde comprend
- Simple : Facile Ă utiliser et Ă comprendre
- Flexible : Fonctionne avec n'importe quel langage de programmation
- Stateless : Chaque requĂȘte est indĂ©pendante
- Scalable : Peut gĂ©rer beaucoup de requĂȘtes simultanĂ©es
Pour apprendre et tester, vous pouvez utiliser des API de test gratuites comme :
- Mazoul API : API de test avec des utilisateurs marocains
- JSONPlaceholder : API de test avec des données fictives
- ReqRes : API de test pour les utilisateurs
5.4.2 â Package http : installation et configuration
đŻ Objectif
Installer le package http dans votre projet Flutter pour pouvoir effectuer des requĂȘtes HTTP vers des API REST.
đŠ Qu'est-ce que le package http ?
Le package http est le package officiel de Flutter pour effectuer des requĂȘtes HTTP. Il permet de communiquer avec des serveurs web pour rĂ©cupĂ©rer ou envoyer des donnĂ©es.
HTTP (HyperText Transfer Protocol) est le protocole utilisé pour communiquer sur Internet. Quand vous visitez un site web ou utilisez une application mobile qui se connecte à Internet, vous utilisez HTTP.
đĄ Installation pas Ă pas
Ătape 1 : Ajouter la dĂ©pendance
Ouvrez le fichier pubspec.yaml de votre projet Flutter et ajoutez la dépendance http dans la section dependencies :
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
La version peut varier. Pour obtenir la derniĂšre version, consultez pub.dev/packages/http ou utilisez la commande :
flutter pub add http
Cette commande ajoute automatiquement la derniĂšre version compatible.
Ătape 2 : Installer le package
Exécutez la commande suivante dans votre terminal, à la racine de votre projet Flutter :
flutter pub get
â
Résultat attendu : Vous devriez voir un message indiquant que le package a été installé avec succÚs.
Ătape 3 : Importer le package
Dans le fichier Dart oĂč vous souhaitez utiliser le package http, ajoutez l'import en haut du fichier :
import 'package:http/http.dart' as http;
import 'dart:convert';
as http ?Le préfixe
as http permet d'éviter les conflits de noms. Vous pouvez ensuite utiliser http.get() au lieu de simplement get(), ce qui rend le code plus clair.Pourquoi
dart:convert ?Le package
dart:convert est nécessaire pour convertir les réponses JSON en objets Dart. Il est inclus par défaut dans Flutter, pas besoin de l'ajouter à pubspec.yaml.
đ VĂ©rifier l'installation
import 'package:http/http.dart' as http;
void main() {
print('Package http est prĂȘt Ă ĂȘtre utilisĂ© !');
}
Si aucune erreur n'apparaĂźt, l'installation est rĂ©ussie. â
đ§ Configuration Android (optionnel)
Pour Android, vous devez autoriser l'accĂšs Ă Internet. Ouvrez le fichier android/app/src/main/AndroidManifest.xml et ajoutez la permission Internet :
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
Sur iOS, aucune configuration supplémentaire n'est nécessaire. L'accÚs à Internet est autorisé par défaut.
Si vous utilisez VS Code ou Android Studio, l'IDE vous proposera automatiquement d'importer le package lorsque vous taperez
http dans votre code.
5.4.3 â Effectuer des requĂȘtes GET
Les requĂȘtes GET permettent de rĂ©cupĂ©rer des donnĂ©es depuis un serveur. C'est la mĂ©thode la plus courante pour obtenir des informations.
đ Qu'est-ce qu'une requĂȘte GET ?
Une requĂȘte GET demande au serveur de renvoyer des donnĂ©es. C'est comme demander Ă quelqu'un de vous donner un livre : vous ne modifiez rien, vous rĂ©cupĂ©rez juste des informations.
đ Syntaxe de base
Voici comment effectuer une requĂȘte GET simple :
import 'package:http/http.dart' as http;
import 'dart:convert';
// Effectuer une requĂȘte GET
Future<void> recupererDonnees() async {
final url = Uri.parse('https://mazoul.online/json/listusers');
final response = await http.get(url);
if (response.statusCode == 200) {
// SuccÚs : traiter les données
print('Données reçues : ${response.body}');
} else {
// Erreur
print('Erreur : ${response.statusCode}');
}
}
Analysons cette syntaxe :
Uri.parse(): Convertit l'URL en objet Uri (requis par le package http)http.get(url): Effectue une requĂȘte GET vers l'URL spĂ©cifiĂ©e (opĂ©ration asynchrone)response.statusCode: Code de statut HTTP (200 = succĂšs, 404 = non trouvĂ©, etc.)response.body: Le corps de la rĂ©ponse (les donnĂ©es reçues, gĂ©nĂ©ralement en JSON)
- 200 : SuccĂšs - La requĂȘte a rĂ©ussi
- 201 : Créé - Une ressource a été créée avec succÚs
- 400 : Mauvaise requĂȘte - La requĂȘte est invalide
- 401 : Non autorisé - Authentification requise
- 404 : Non trouvé - La ressource demandée n'existe pas
- 500 : Erreur serveur - Le serveur a rencontré une erreur
đ§Ș Exemple : RĂ©cupĂ©rer et afficher des donnĂ©es
Voici un exemple complet qui récupÚre des données depuis une API et les affiche dans l'interface :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
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 _donnees = 'Cliquez sur le bouton pour charger les données';
bool _chargement = false;
// Récupérer des données depuis l'API
Future<void> chargerDonnees() async {
setState(() {
_chargement = true;
});
try {
// URL de l'API
final url = Uri.parse('https://mazoul.online/json/listusers');
// Effectuer la requĂȘte GET
final response = await http.get(url);
if (response.statusCode == 200) {
// Convertir la réponse JSON en List (tableau d'utilisateurs)
final List<dynamic> data = json.decode(response.body);
if (data.isNotEmpty) {
// Afficher le premier utilisateur
final premierUtilisateur = data[0] as Map<String, dynamic>;
setState(() {
_donnees = 'Nom : ${premierUtilisateur['nom']} ${premierUtilisateur['prenom']}\n'
'Email : ${premierUtilisateur['email']}\n'
'Ville : ${premierUtilisateur['ville']}\n'
'Total utilisateurs : ${data.length}';
_chargement = false;
});
} else {
setState(() {
_donnees = 'Aucun utilisateur trouvé';
_chargement = false;
});
}
} else {
setState(() {
_donnees = 'Erreur : ${response.statusCode}';
_chargement = false;
});
}
} catch (e) {
setState(() {
_donnees = 'Erreur de connexion : $e';
_chargement = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RequĂȘte GET'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Afficher les données
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: _chargement
? const Center(child: CircularProgressIndicator())
: Text(
_donnees,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 24),
// Bouton pour charger les données
ElevatedButton(
onPressed: _chargement ? null : chargerDonnees,
child: const Text('Charger les données'),
),
],
),
),
),
);
}
}
Dans cet exemple :
- Un bouton dĂ©clenche la requĂȘte GET
- Un indicateur de chargement s'affiche pendant la requĂȘte
- Les données JSON sont converties en Map Dart
- Les données sont affichées dans l'interface
- Les erreurs sont gérées avec try-catch
json.decode() convertit une chaĂźne JSON en objet Dart (Map ou List). C'est l'inverse de json.encode() qui convertit un objet Dart en chaĂźne JSON.
đ RĂ©cupĂ©rer une liste de donnĂ©es
Pour récupérer plusieurs éléments (une liste), le processus est similaire :
Future<List<Map<String, dynamic>>> recupererListe() async {
final url = Uri.parse('https://mazoul.online/posts');
final response = await http.get(url);
if (response.statusCode == 200) {
// Convertir la réponse JSON en List
final List<dynamic> data = json.decode(response.body);
return data.cast<Map<String, dynamic>>();
} else {
throw Exception('Erreur : ${response.statusCode}');
}
}
N'oubliez pas que les requĂȘtes HTTP sont asynchrones. Vous devez toujours utiliser
await et déclarer votre fonction comme async. De plus, vérifiez toujours le statusCode avant de traiter les données.
5.4.4 â Envoyer des donnĂ©es avec POST, PUT, DELETE
En plus de récupérer des données (GET), vous pouvez créer, modifier ou supprimer des données avec POST, PUT et DELETE.
â POST - CrĂ©er de nouvelles donnĂ©es
La méthode POST permet d'envoyer de nouvelles données au serveur pour les créer.
đ Syntaxe
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> creerDonnee() async {
final url = Uri.parse('https://mazoul.online/posts');
// Données à envoyer (converties en JSON)
final donnees = {
'title': 'Mon titre',
'body': 'Mon contenu',
'userId': 1,
};
// Headers pour indiquer que nous envoyons du JSON
final headers = {
'Content-Type': 'application/json',
};
// Effectuer la requĂȘte POST
final response = await http.post(
url,
headers: headers,
body: json.encode(donnees),
);
if (response.statusCode == 201) {
print('Données créées : ${response.body}');
} else {
print('Erreur : ${response.statusCode}');
}
}
âïž PUT - Modifier des donnĂ©es existantes
La méthode PUT permet de modifier des données existantes sur le serveur.
Future<void> modifierDonnee(int id) async {
final url = Uri.parse('https://mazoul.online/posts/$id');
// Nouvelles données
final donnees = {
'title': 'Titre modifié',
'body': 'Contenu modifié',
'userId': 1,
};
final headers = {
'Content-Type': 'application/json',
};
// Effectuer la requĂȘte PUT
final response = await http.put(
url,
headers: headers,
body: json.encode(donnees),
);
if (response.statusCode == 200) {
print('Données modifiées : ${response.body}');
}
}
đïž DELETE - Supprimer des donnĂ©es
La méthode DELETE permet de supprimer des données sur le serveur.
Future<void> supprimerDonnee(int id) async {
final url = Uri.parse('https://mazoul.online/posts/$id');
// Effectuer la requĂȘte DELETE
final response = await http.delete(url);
if (response.statusCode == 200) {
print('Données supprimées');
}
}
đ§Ș Exemple : Application CRUD complĂšte
Voici un exemple complet qui montre toutes les opérations CRUD avec une API :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
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 _titreController = TextEditingController();
final _contenuController = TextEditingController();
String _message = '';
bool _chargement = false;
// POST - Créer une nouvelle donnée
Future<void> creerDonnee() async {
if (_titreController.text.isEmpty || _contenuController.text.isEmpty) {
setState(() {
_message = 'Veuillez remplir tous les champs';
});
return;
}
setState(() {
_chargement = true;
_message = '';
});
try {
final url = Uri.parse('https://mazoul.online/posts');
final donnees = {
'title': _titreController.text,
'body': _contenuController.text,
'userId': 1,
};
final headers = {'Content-Type': 'application/json'};
final response = await http.post(
url,
headers: headers,
body: json.encode(donnees),
);
if (response.statusCode == 201) {
final resultat = json.decode(response.body);
setState(() {
_message = 'Créé avec succÚs ! ID: ${resultat['id']}';
_titreController.clear();
_contenuController.clear();
});
} else {
setState(() {
_message = 'Erreur : ${response.statusCode}';
});
}
} catch (e) {
setState(() {
_message = 'Erreur : $e';
});
} finally {
setState(() {
_chargement = false;
});
}
}
// PUT - Modifier une donnée (exemple avec ID 1)
Future<void> modifierDonnee() async {
if (_titreController.text.isEmpty || _contenuController.text.isEmpty) {
setState(() {
_message = 'Veuillez remplir tous les champs';
});
return;
}
setState(() {
_chargement = true;
_message = '';
});
try {
final url = Uri.parse('https://mazoul.online/json/listusers');
final donnees = {
'title': _titreController.text,
'body': _contenuController.text,
'userId': 1,
};
final headers = {'Content-Type': 'application/json'};
final response = await http.put(
url,
headers: headers,
body: json.encode(donnees),
);
if (response.statusCode == 200) {
setState(() {
_message = 'Modifié avec succÚs !';
_titreController.clear();
_contenuController.clear();
});
} else {
setState(() {
_message = 'Erreur : ${response.statusCode}';
});
}
} catch (e) {
setState(() {
_message = 'Erreur : $e';
});
} finally {
setState(() {
_chargement = false;
});
}
}
// DELETE - Supprimer une donnée (exemple avec ID 1)
Future<void> supprimerDonnee() async {
setState(() {
_chargement = true;
_message = '';
});
try {
final url = Uri.parse('https://mazoul.online/json/listusers');
final response = await http.delete(url);
if (response.statusCode == 200) {
setState(() {
_message = 'Supprimé avec succÚs !';
});
} else {
setState(() {
_message = 'Erreur : ${response.statusCode}';
});
}
} catch (e) {
setState(() {
_message = 'Erreur : $e';
});
} finally {
setState(() {
_chargement = false;
});
}
}
@override
void dispose() {
_titreController.dispose();
_contenuController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('POST, PUT, DELETE'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Formulaire
TextField(
controller: _titreController,
decoration: const InputDecoration(
labelText: 'Titre',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _contenuController,
decoration: const InputDecoration(
labelText: 'Contenu',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 24),
// Boutons
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _chargement ? null : creerDonnee,
child: const Text('POST (Créer)'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _chargement ? null : modifierDonnee,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('PUT (Modifier)'),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _chargement ? null : supprimerDonnee,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('DELETE (Supprimer)'),
),
),
const SizedBox(height: 24),
// Message
if (_chargement)
const CircularProgressIndicator()
else if (_message.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _message.contains('succĂšs')
? Colors.green[100]
: Colors.red[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_message,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
],
),
),
),
);
}
}
Le header
Content-Type: application/json indique au serveur que nous envoyons des données au format JSON. C'est important pour que le serveur puisse correctement interpréter les données.
- POST et PUT nécessitent un
bodyavec les donnĂ©es Ă envoyer - Les donnĂ©es doivent ĂȘtre converties en JSON avec
json.encode() - DELETE ne nécessite généralement pas de body
- Toujours vérifier le
statusCodepour confirmer le succÚs de l'opération
5.4.5 â Traiter les rĂ©ponses JSON
Les API REST renvoient généralement des données au format JSON. Il est important de savoir convertir ces données JSON en objets Dart pour les utiliser dans votre application.
đŠ Qu'est-ce que JSON ?
JSON (JavaScript Object Notation) est un format de données textuel simple et lisible. Il ressemble à un objet JavaScript ou à un dictionnaire Python.
{
"id": 1,
"nom": "Jean Dupont",
"email": "jean@example.com",
"age": 30,
"actif": true
}
đ Convertir JSON en Dart
Pour utiliser les données JSON dans Dart, vous devez les convertir en objets Dart :
1. JSON simple (Map)
// Réponse JSON
String jsonString = '{"nom": "Jean", "age": 30}';
// Convertir en Map
Map<String, dynamic> data = json.decode(jsonString);
// Utiliser les données
String nom = data['nom']; // "Jean"
int age = data['age']; // 30
2. JSON avec liste
// Réponse JSON avec liste
String jsonString = '[{"nom": "Jean"}, {"nom": "Marie"}]';
// Convertir en List
List<dynamic> data = json.decode(jsonString);
// Accéder aux éléments
String premierNom = data[0]['nom']; // "Jean"
String deuxiemeNom = data[1]['nom']; // "Marie"
đ§Ș Exemple : CrĂ©er un modĂšle et traiter JSON
La meilleure pratique est de créer un modÚle (classe) pour représenter vos données :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// ModÚle pour représenter un utilisateur
class Utilisateur {
final int id;
final String nom;
final String prenom;
final String email;
final String telephone;
final String ville;
final String dateInscription;
Utilisateur({
required this.id,
required this.nom,
required this.prenom,
required this.email,
required this.telephone,
required this.ville,
required this.dateInscription,
});
// Convertir JSON en Utilisateur
factory Utilisateur.fromJson(Map<String, dynamic> json) {
return Utilisateur(
id: json['id'] as int,
nom: json['nom'] as String,
prenom: json['prenom'] as String,
email: json['email'] as String,
telephone: json['telephone'] as String,
ville: json['ville'] as String,
dateInscription: json['dateInscription'] as String,
);
}
// Convertir Utilisateur en JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'nom': nom,
'prenom': prenom,
'email': email,
'telephone': telephone,
'ville': ville,
'dateInscription': dateInscription,
};
}
}
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> {
List<Utilisateur> _utilisateurs = [];
bool _chargement = false;
// Récupérer et traiter les données JSON
Future<void> chargerUtilisateurs() async {
setState(() {
_chargement = true;
});
try {
// Récupérer les données depuis l'API
final response = await http.get(
Uri.parse('https://mazoul.online/json/listusers'),
);
if (response.statusCode == 200) {
// Convertir la réponse JSON en List
final List<dynamic> jsonData = json.decode(response.body);
// Convertir chaque élément JSON en Utilisateur
final utilisateurs = jsonData.map((json) {
return Utilisateur.fromJson(json as Map<String, dynamic>);
}).toList();
setState(() {
_utilisateurs = utilisateurs;
_chargement = false;
});
} else {
setState(() {
_chargement = false;
});
}
} catch (e) {
setState(() {
_chargement = false;
});
print('Erreur : $e');
}
}
@override
void initState() {
super.initState();
chargerUtilisateurs();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Traiter JSON'),
),
body: SafeArea(
child: _chargement
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _utilisateurs.length,
itemBuilder: (context, index) {
final utilisateur = _utilisateurs[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
title: Text('${utilisateur.prenom} ${utilisateur.nom}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(utilisateur.email),
Text('${utilisateur.ville} - ${utilisateur.telephone}'),
],
),
leading: CircleAvatar(
child: Text(utilisateur.id.toString()),
),
),
);
},
),
),
);
}
}
đ MĂ©thodes utiles pour JSON
json.decode()json.decode('{"nom": "Jean"}')json.encode()json.encode({'nom': 'Jean'})jsonEncode()jsonEncode({'nom': 'Jean'})jsonDecode()jsonDecode('{"nom": "Jean"}')Les méthodes
fromJson() et toJson() sont des conventions courantes pour convertir entre JSON et objets Dart. Cela rend votre code plus lisible et maintenable.
Lors de la conversion JSON, utilisez toujours des casts explicites (
as String, as int) et gérez les valeurs nulles avec l'opérateur ?? pour éviter les erreurs à l'exécution.
5.4.6 â Gestion des erreurs rĂ©seau
Les requĂȘtes rĂ©seau peuvent Ă©chouer pour diverses raisons. Il est essentiel de gĂ©rer ces erreurs pour offrir une bonne expĂ©rience utilisateur.
â ïž Types d'erreurs courantes
Voici les erreurs les plus courantes lors des requĂȘtes rĂ©seau :
- Pas de connexion Internet : L'appareil n'est pas connecté au réseau
- Timeout : La requĂȘte prend trop de temps
- Erreur serveur : Le serveur renvoie une erreur (500, 503, etc.)
- Ressource non trouvée : L'URL n'existe pas (404)
- Non autorisé : Authentification requise (401, 403)
đĄïž GĂ©rer les erreurs avec try-catch
La meilleure façon de gérer les erreurs est d'utiliser try-catch :
Future<void> recupererDonnees() async {
try {
final url = Uri.parse('https://api.exemple.com/data');
final response = await http.get(url);
if (response.statusCode == 200) {
// SuccĂšs
print('Données : ${response.body}');
} else {
// Erreur HTTP (404, 500, etc.)
print('Erreur HTTP : ${response.statusCode}');
}
} on SocketException {
// Pas de connexion Internet
print('Pas de connexion Internet');
} on TimeoutException {
// Timeout
print('La requĂȘte a pris trop de temps');
} on HttpException {
// Erreur HTTP
print('Erreur HTTP');
} catch (e) {
// Autre erreur
print('Erreur inattendue : $e');
}
}
đ§Ș Exemple : Gestion complĂšte des erreurs
Voici un exemple complet avec une gestion d'erreurs robuste :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
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 _message = 'Cliquez pour charger';
bool _chargement = false;
Future<void> chargerDonnees() async {
setState(() {
_chargement = true;
_message = 'Chargement...';
});
try {
final url = Uri.parse('https://mazoul.online/json/listusers');
// Ajouter un timeout
final response = await http.get(url).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('La requĂȘte a pris trop de temps');
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_message = 'SuccĂšs !\nTitre : ${data['title']}';
_chargement = false;
});
} else {
setState(() {
_message = 'Erreur serveur : ${response.statusCode}';
_chargement = false;
});
}
} on SocketException {
setState(() {
_message = 'Erreur : Pas de connexion Internet';
_chargement = false;
});
} on TimeoutException catch (e) {
setState(() {
_message = 'Erreur : ${e.message}';
_chargement = false;
});
} on HttpException catch (e) {
setState(() {
_message = 'Erreur HTTP : ${e.message}';
_chargement = false;
});
} catch (e) {
setState(() {
_message = 'Erreur inattendue : $e';
_chargement = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gestion des erreurs'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _message.contains('SuccĂšs')
? Colors.green[100]
: _message.contains('Erreur')
? Colors.red[100]
: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: _chargement
? const Center(child: CircularProgressIndicator())
: Text(
_message,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _chargement ? null : chargerDonnees,
child: const Text('Charger les données'),
),
],
),
),
),
);
}
}
// Exception personnalisée pour Timeout
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
}
đ Bonnes pratiques
- Toujours utiliser try-catch : Entourez vos requĂȘtes HTTP de blocs try-catch
- Vérifier le statusCode : Ne traitez les données que si le code est 200 ou 201
- Ajouter un timeout : Ăvitez que les requĂȘtes attendent indĂ©finiment
- Messages d'erreur clairs : Informez l'utilisateur de ce qui s'est passé
- Gérer les cas spécifiques : Utilisez des exceptions spécifiques (SocketException, TimeoutException)
Le timeout permet de limiter le temps d'attente d'une requĂȘte. Si la requĂȘte prend plus de temps que le timeout spĂ©cifiĂ©, une exception est levĂ©e. C'est important pour Ă©viter que l'application reste bloquĂ©e indĂ©finiment.
N'oubliez pas d'importer
dart:io pour utiliser SocketException et HttpException. Ces exceptions sont essentielles pour gérer les erreurs réseau de maniÚre appropriée.
5.4.7 â Authentification et headers
De nombreuses API nécessitent une authentification pour accéder aux données. L'authentification se fait généralement via des headers HTTP.
đ Types d'authentification courants
1. API Key
Une clé API simple dans les headers :
final headers = {
'X-API-Key': 'votre_cle_api_ici',
};
final response = await http.get(
Uri.parse('https://api.exemple.com/data'),
headers: headers,
);
2. Bearer Token (JWT)
Un token d'authentification (souvent un JWT) :
final headers = {
'Authorization': 'Bearer votre_token_ici',
'Content-Type': 'application/json',
};
final response = await http.get(
Uri.parse('https://api.exemple.com/data'),
headers: headers,
);
3. Basic Authentication
Nom d'utilisateur et mot de passe encodés en base64 :
import 'dart:convert';
String username = 'utilisateur';
String password = 'motdepasse';
String credentials = base64Encode(utf8.encode('$username:$password'));
final headers = {
'Authorization': 'Basic $credentials',
};
final response = await http.get(
Uri.parse('https://api.exemple.com/data'),
headers: headers,
);
đ§Ș Exemple : Authentification avec token
Voici un exemple complet avec authentification par token :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
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 _tokenController = TextEditingController();
String _donnees = '';
bool _chargement = false;
// Récupérer des données avec authentification
Future<void> recupererDonneesAvecAuth() async {
if (_tokenController.text.isEmpty) {
setState(() {
_donnees = 'Veuillez entrer un token';
});
return;
}
setState(() {
_chargement = true;
_donnees = '';
});
try {
// Headers avec authentification
final headers = {
'Authorization': 'Bearer ${_tokenController.text}',
'Content-Type': 'application/json',
};
final url = Uri.parse('https://api.exemple.com/protected-data');
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_donnees = 'Données récupérées : ${data.toString()}';
});
} else if (response.statusCode == 401) {
setState(() {
_donnees = 'Erreur : Token invalide ou expiré';
});
} else {
setState(() {
_donnees = 'Erreur : ${response.statusCode}';
});
}
} catch (e) {
setState(() {
_donnees = 'Erreur : $e';
});
} finally {
setState(() {
_chargement = false;
});
}
}
@override
void dispose() {
_tokenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Authentification'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Token d\'authentification',
border: OutlineInputBorder(),
hintText: 'Entrez votre token',
),
obscureText: true,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _chargement ? null : recupererDonneesAvecAuth,
child: const Text('Récupérer les données'),
),
const SizedBox(height: 24),
if (_chargement)
const CircularProgressIndicator()
else if (_donnees.isNotEmpty)
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(
_donnees,
style: const TextStyle(fontSize: 16),
),
),
),
),
],
),
),
),
);
}
}
đ Headers courants
'Content-Type': 'application/json''Authorization': 'Bearer token123''Accept': 'application/json''X-API-Key': 'ma_cle'"Bearer" signifie "porteur" en anglais. C'est un type d'authentification oĂč vous "portez" un token qui prouve votre identitĂ©. Le format standard est
Bearer votre_token.
Ne stockez jamais les tokens ou clés API directement dans le code source. Utilisez plutÎt Secure Storage (voir chapitre 5.2) ou des variables d'environnement pour les stocker de maniÚre sécurisée.
5.4.8 â Bonnes pratiques et async/await
Pour créer des applications robustes qui communiquent avec des API, il est important de suivre certaines bonnes pratiques et de bien comprendre async/await.
đ Comprendre async/await
Les requĂȘtes rĂ©seau sont asynchrones : elles prennent du temps et ne bloquent pas l'application pendant l'attente.
Imaginez que vous commandez une pizza :
- Sans async : Vous attendez devant le restaurant sans pouvoir faire autre chose
- Avec async : Vous allez faire vos courses pendant que la pizza est prĂ©parĂ©e, puis vous revenez la chercher quand elle est prĂȘte
đ Syntaxe async/await
// Fonction asynchrone
Future<void> maFonction() async {
// Opération asynchrone avec await
final response = await http.get(url);
// Le code ici attend que la requĂȘte soit terminĂ©e
print('Réponse reçue : ${response.body}');
}
// Appeler une fonction asynchrone
void appelerFonction() {
maFonction(); // Ne bloque pas l'application
}
â Bonnes pratiques
1. Créer une classe de service
Organisez vos appels API dans une classe dédiée :
class ApiService {
static const String baseUrl = 'https://api.exemple.com';
// Récupérer des données
static Future<Map<String, dynamic>> getData() async {
final url = Uri.parse('$baseUrl/data');
final response = await http.get(url);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Erreur : ${response.statusCode}');
}
}
// Créer des données
static Future<void> createData(Map<String, dynamic> data) async {
final url = Uri.parse('$baseUrl/data');
final headers = {'Content-Type': 'application/json'};
final response = await http.post(
url,
headers: headers,
body: json.encode(data),
);
if (response.statusCode != 201) {
throw Exception('Erreur : ${response.statusCode}');
}
}
}
2. Gérer les erreurs proprement
Créez des exceptions personnalisées pour mieux gérer les erreurs :
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, [this.statusCode]);
@override
String toString() => 'ApiException: $message (${statusCode ?? 'N/A'})';
}
// Utilisation
Future<void> appelerApi() async {
try {
final response = await http.get(url);
if (response.statusCode == 200) {
// SuccĂšs
} else {
throw ApiException('Erreur serveur', response.statusCode);
}
} catch (e) {
if (e is ApiException) {
print('Erreur API : ${e.message}');
} else {
print('Erreur inattendue : $e');
}
}
}
3. Utiliser des modĂšles
Créez des modÚles pour vos données :
class Article {
final int id;
final String titre;
final String contenu;
Article({required this.id, required this.titre, required this.contenu});
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'],
titre: json['titre'],
contenu: json['contenu'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'titre': titre,
'contenu': contenu,
};
}
}
đ§Ș Exercice pratique : CrĂ©er votre propre API avec XAMPP + PHP + MySQL
Dans cet exercice, vous allez créer votre propre API REST avec XAMPP, PHP et MySQL, puis la connecter à votre application Flutter. Cet exemple simple couvre 80% de ce que vous utiliserez dans des applications réelles.
đ PrĂ©requis
- XAMPP installé et démarré (Apache + MySQL)
- Un éditeur de code (VS Code, PHPStorm, etc.)
- Votre application Flutter prĂȘte
đïž Ătape 1 : CrĂ©er la base de donnĂ©es MySQL
Créez une base de données et une table Personne :
-- Créer la base de données
CREATE DATABASE IF NOT EXISTS api_flutter;
USE api_flutter;
-- Créer la table Personne
CREATE TABLE IF NOT EXISTS personne (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
prenom VARCHAR(50) NOT NULL,
age INT NOT NULL,
genre VARCHAR(10) NOT NULL,
tel VARCHAR(20) NOT NULL,
date_creation TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- id : Identifiant unique (auto-incrémenté)
- nom : Nom de la personne
- prenom : Prénom de la personne
- age : Ăge (nombre entier)
- genre : Genre (Homme, Femme, Autre)
- tel : Numéro de téléphone
- date_creation : Date de création automatique
đ Ătape 2 : CrĂ©er les fichiers PHP
Placez ces fichiers dans le dossier htdocs de XAMPP (ex: C:\xampp\htdocs\api_flutter\).
Fichier 1 : config.php (Configuration de la base de données)
<?php
// Configuration de la base de données
define('DB_HOST', 'localhost');
define('DB_NAME', 'api_flutter');
define('DB_USER', 'root');
define('DB_PASS', ''); // Mot de passe vide par défaut dans XAMPP
// Connexion à la base de données
function getConnection() {
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8",
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
return $pdo;
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['erreur' => 'Erreur de connexion à la base de données']);
exit;
}
}
// Headers pour permettre les requĂȘtes depuis Flutter
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
?>
Fichier 2 : get_personnes.php (GET - Récupérer toutes les personnes)
<?php
require_once 'config.php';
// Vérifier que la méthode est GET
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['erreur' => 'Méthode non autorisée']);
exit;
}
try {
$pdo = getConnection();
// RequĂȘte SQL pour rĂ©cupĂ©rer toutes les personnes
$stmt = $pdo->query('SELECT * FROM personne ORDER BY id DESC');
$personnes = $stmt->fetchAll();
// Retourner les données en JSON
http_response_code(200);
echo json_encode($personnes, JSON_UNESCAPED_UNICODE);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['erreur' => 'Erreur lors de la récupération des données']);
}
?>
Fichier 3 : post_personne.php (POST - Créer une nouvelle personne)
<?php
require_once 'config.php';
// Vérifier que la méthode est POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['erreur' => 'Méthode non autorisée']);
exit;
}
try {
// Récupérer les données JSON envoyées
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Valider les données
if (!isset($data['nom']) || !isset($data['prenom']) ||
!isset($data['age']) || !isset($data['genre']) || !isset($data['tel'])) {
http_response_code(400);
echo json_encode(['erreur' => 'Données incomplÚtes']);
exit;
}
// Nettoyer et valider les données
$nom = trim($data['nom']);
$prenom = trim($data['prenom']);
$age = intval($data['age']);
$genre = trim($data['genre']);
$tel = trim($data['tel']);
// Validation supplémentaire
if (empty($nom) || empty($prenom) || $age < 0 || $age > 150) {
http_response_code(400);
echo json_encode(['erreur' => 'Données invalides']);
exit;
}
$pdo = getConnection();
// Insérer la nouvelle personne
$stmt = $pdo->prepare('INSERT INTO personne (nom, prenom, age, genre, tel) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$nom, $prenom, $age, $genre, $tel]);
// Récupérer l'ID de la personne créée
$id = $pdo->lastInsertId();
// Retourner la personne créée
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Personne créée avec succÚs',
'personne' => [
'id' => $id,
'nom' => $nom,
'prenom' => $prenom,
'age' => $age,
'genre' => $genre,
'tel' => $tel
]
], JSON_UNESCAPED_UNICODE);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['erreur' => 'Erreur lors de la création']);
}
?>
- CORS : Les headers
Access-Control-Allow-Originpermettent à Flutter d'accéder à l'API - Validation : Les données sont validées avant insertion
- PrĂ©paration des requĂȘtes : Utilisation de
prepare()pour éviter les injections SQL - Codes HTTP : Retour de codes appropriés (200, 201, 400, 500)
Pour que votre application Flutter puisse accéder à l'API sur votre ordinateur :
- Trouver votre adresse IP locale :
- Windows : Ouvrez CMD et tapez
ipconfig, cherchez "IPv4 Address" - Mac/Linux : Ouvrez Terminal et tapez
ifconfigouip addr
- Windows : Ouvrez CMD et tapez
- Modifier le code Flutter : Remplacez
192.168.1.100par votre adresse IP réelle - Vérifier le firewall : Assurez-vous que le port 80 (Apache) n'est pas bloqué
- Pour Android : Utilisez
10.0.2.2si vous testez sur un émulateur Android
đ± Ătape 3 : Code Flutter
Voici le code Flutter complet pour interagir avec votre API :
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
// ModĂšle Personne
class Personne {
final int id;
final String nom;
final String prenom;
final int age;
final String genre;
final String tel;
Personne({
required this.id,
required this.nom,
required this.prenom,
required this.age,
required this.genre,
required this.tel,
});
// Convertir JSON en Personne
factory Personne.fromJson(Map<String, dynamic> json) {
return Personne(
id: json['id'] as int,
nom: json['nom'] as String,
prenom: json['prenom'] as String,
age: json['age'] as int,
genre: json['genre'] as String,
tel: json['tel'] as String,
);
}
// Convertir Personne en JSON
Map<String, dynamic> toJson() {
return {
'nom': nom,
'prenom': prenom,
'age': age,
'genre': genre,
'tel': tel,
};
}
}
// Service API
class ApiService {
// â ïž Remplacez par votre adresse IP locale (ex: http://192.168.1.100/api_flutter)
// Pour trouver votre IP : ipconfig (Windows) ou ifconfig (Mac/Linux)
static const String baseUrl = 'http://192.168.1.100/api_flutter';
// GET - Récupérer toutes les personnes
static Future<List<Personne>> getPersonnes() async {
try {
final url = Uri.parse('$baseUrl/get_personnes.php');
final response = await http.get(url).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((json) => Personne.fromJson(json as Map<String, dynamic>)).toList();
} else {
throw Exception('Erreur : ${response.statusCode}');
}
} on SocketException {
throw Exception('Impossible de se connecter au serveur. Vérifiez que XAMPP est démarré.');
} on TimeoutException {
throw Exception('La requĂȘte a pris trop de temps');
} catch (e) {
throw Exception('Erreur : $e');
}
}
// POST - Créer une nouvelle personne
static Future<Personne> createPersonne(Personne personne) async {
try {
final url = Uri.parse('$baseUrl/post_personne.php');
final headers = {'Content-Type': 'application/json'};
final response = await http.post(
url,
headers: headers,
body: json.encode(personne.toJson()),
).timeout(const Duration(seconds: 10));
if (response.statusCode == 201) {
final data = json.decode(response.body);
final personneData = data['personne'] as Map<String, dynamic>;
return Personne.fromJson(personneData);
} else {
final error = json.decode(response.body);
throw Exception(error['erreur'] ?? 'Erreur lors de la création');
}
} on SocketException {
throw Exception('Impossible de se connecter au serveur');
} on TimeoutException {
throw Exception('La requĂȘte a pris trop de temps');
} catch (e) {
throw Exception('Erreur : $e');
}
}
}
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> {
List<Personne> _personnes = [];
bool _chargement = false;
String? _erreur;
// ContrĂŽleurs pour le formulaire
final _nomController = TextEditingController();
final _prenomController = TextEditingController();
final _ageController = TextEditingController();
final _telController = TextEditingController();
String _genreSelectionne = 'Homme';
Future<void> chargerPersonnes() async {
setState(() {
_chargement = true;
_erreur = null;
});
try {
final personnes = await ApiService.getPersonnes();
setState(() {
_personnes = personnes;
_chargement = false;
});
} catch (e) {
setState(() {
_erreur = e.toString();
_chargement = false;
});
}
}
Future<void> ajouterPersonne() async {
if (_nomController.text.isEmpty ||
_prenomController.text.isEmpty ||
_ageController.text.isEmpty ||
_telController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Veuillez remplir tous les champs')),
);
return;
}
setState(() {
_chargement = true;
_erreur = null;
});
try {
final nouvellePersonne = Personne(
id: 0, // Sera généré par la base de données
nom: _nomController.text,
prenom: _prenomController.text,
age: int.parse(_ageController.text),
genre: _genreSelectionne,
tel: _telController.text,
);
await ApiService.createPersonne(nouvellePersonne);
// Vider les champs
_nomController.clear();
_prenomController.clear();
_ageController.clear();
_telController.clear();
// Recharger la liste
chargerPersonnes();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Personne ajoutée avec succÚs !')),
);
}
} catch (e) {
setState(() {
_erreur = e.toString();
_chargement = false;
});
}
}
@override
void initState() {
super.initState();
chargerPersonnes();
}
@override
void dispose() {
_nomController.dispose();
_prenomController.dispose();
_ageController.dispose();
_telController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gestion des personnes'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: chargerPersonnes,
),
],
),
body: SafeArea(
child: Column(
children: [
// Formulaire d'ajout
Card(
margin: const EdgeInsets.all(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'Ajouter une personne',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _ageController,
decoration: const InputDecoration(
labelText: 'Ăge',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
Expanded(
child: DropdownButtonFormField<String>(
value: _genreSelectionne,
decoration: const InputDecoration(
labelText: 'Genre',
border: OutlineInputBorder(),
),
items: ['Homme', 'Femme', 'Autre']
.map((genre) => DropdownMenuItem(
value: genre,
child: Text(genre),
))
.toList(),
onChanged: (value) {
setState(() {
_genreSelectionne = value!;
});
},
),
),
],
),
const SizedBox(height: 8),
TextField(
controller: _telController,
decoration: const InputDecoration(
labelText: 'Téléphone',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _chargement ? null : ajouterPersonne,
child: const Text('Ajouter'),
),
],
),
),
),
// Liste des personnes
Expanded(
child: _chargement
? const Center(child: CircularProgressIndicator())
: _erreur != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
_erreur!,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: chargerPersonnes,
child: const Text('Réessayer'),
),
],
),
)
: _personnes.isEmpty
? const Center(
child: Text('Aucune personne enregistrée'),
)
: ListView.builder(
itemCount: _personnes.length,
itemBuilder: (context, index) {
final personne = _personnes[index];
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: ListTile(
title: Text(
'${personne.prenom} ${personne.nom}'),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('${personne.age} ans - ${personne.genre}'),
Text('Tel: ${personne.tel}'),
],
),
leading: CircleAvatar(
child: Text(personne.id.toString()),
),
),
);
},
),
),
],
),
),
);
}
}
// Exception pour timeout
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
}
đ Ătape 4 : AmĂ©liorations avec Token et SĂ©curitĂ©
Pour sécuriser votre API, voici des améliorations essentielles :
1. Authentification par Token (JWT)
Créez un fichier auth.php pour gérer l'authentification :
<?php
// auth.php - Gestion de l'authentification simple
// Clé secrÚte (à changer en production)
define('SECRET_KEY', 'votre_cle_secrete_tres_longue_et_aleatoire');
// Fonction pour générer un token simple
function generateToken($userId) {
$header = base64_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
$payload = base64_encode(json_encode([
'user_id' => $userId,
'exp' => time() + (24 * 60 * 60) // Expire dans 24h
]));
$signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
$signature = base64_encode($signature);
return "$header.$payload.$signature";
}
// Fonction pour vérifier un token
function verifyToken($token) {
$parts = explode('.', $token);
if (count($parts) !== 3) {
return false;
}
list($header, $payload, $signature) = $parts;
$expectedSignature = base64_encode(
hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
);
if ($signature !== $expectedSignature) {
return false;
}
$payloadData = json_decode(base64_decode($payload), true);
// Vérifier l'expiration
if (isset($payloadData['exp']) && $payloadData['exp'] < time()) {
return false;
}
return $payloadData;
}
// Fonction pour obtenir le token depuis les headers
function getTokenFromHeaders() {
$headers = getallheaders();
if (isset($headers['Authorization'])) {
$auth = $headers['Authorization'];
if (preg_match('/Bearer\s+(.*)$/i', $auth, $matches)) {
return $matches[1];
}
}
return null;
}
// Fonction pour vérifier l'authentification
function requireAuth() {
$token = getTokenFromHeaders();
if (!$token) {
http_response_code(401);
echo json_encode(['erreur' => 'Token manquant']);
exit;
}
$payload = verifyToken($token);
if (!$payload) {
http_response_code(401);
echo json_encode(['erreur' => 'Token invalide ou expiré']);
exit;
}
return $payload;
}
?>
2. Fichier PHP sécurisé avec authentification
Modifiez post_personne.php pour exiger un token :
<?php
require_once 'config.php';
require_once 'auth.php';
// Vérifier l'authentification
$user = requireAuth();
// Le reste du code reste identique...
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['erreur' => 'Méthode non autorisée']);
exit;
}
// ... (code d'insertion identique)
?>
3. Améliorations de sécurité supplémentaires
- HTTPS : Utilisez HTTPS en production (certificat SSL)
- Validation stricte : Validez et nettoyez toutes les entrées
- Rate limiting : Limitez le nombre de requĂȘtes par IP
- Hachage des mots de passe : Utilisez
password_hash()etpassword_verify() - PrĂ©paration des requĂȘtes : Toujours utiliser
prepare()pour éviter les injections SQL - Sanitization : Utilisez
htmlspecialchars()etfilter_var() - Logs : Enregistrez les tentatives d'accĂšs suspectes
4. Code Flutter avec authentification
Adaptez votre code Flutter pour envoyer le token :
// Dans ApiService, ajoutez une méthode pour obtenir le token
static String? _token;
static Future<void> login(String username, String password) async {
// Appel Ă votre API de login
final response = await http.post(
Uri.parse('$baseUrl/login.php'),
body: json.encode({'username': username, 'password': password}),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
_token = data['token'];
// Sauvegarder le token avec Secure Storage
final storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: _token!);
}
}
// Modifier createPersonne pour inclure le token
static Future<Personne> createPersonne(Personne personne) async {
// Récupérer le token depuis Secure Storage
final storage = FlutterSecureStorage();
final token = await storage.read(key: 'auth_token');
if (token == null) {
throw Exception('Non authentifié');
}
final headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
// ... reste du code
}
- Token JWT : Permet d'authentifier les utilisateurs sans stocker de session cÎté serveur
- HTTPS : Chiffre les données en transit
- Validation : EmpĂȘche les donnĂ©es malveillantes d'atteindre la base de donnĂ©es
- Rate limiting : ProtÚge contre les attaques par déni de service (DDoS)
â RĂ©sumĂ© de l'exercice
Cet exercice vous a permis de :
- â CrĂ©er une base de donnĂ©es MySQL avec une table structurĂ©e
- â DĂ©velopper une API REST avec PHP (GET et POST)
- â Connecter votre application Flutter Ă l'API
- â GĂ©rer les erreurs et les Ă©tats de chargement
- â Comprendre les bases de la sĂ©curitĂ© (tokens, validation)
- â Structure de base de donnĂ©es
- â API REST (GET, POST)
- â Authentification basique
- â Validation des donnĂ©es
- â Gestion d'erreurs
- â Interface Flutter complĂšte
đ Checklist des bonnes pratiques
- â Toutes les requĂȘtes sont dans des blocs try-catch
- â Les timeouts sont configurĂ©s
- â Les erreurs sont gĂ©rĂ©es et affichĂ©es Ă l'utilisateur
- â Les modĂšles sont utilisĂ©s pour structurer les donnĂ©es
- â Les appels API sont organisĂ©s dans une classe de service
- â Les tokens d'authentification sont stockĂ©s de maniĂšre sĂ©curisĂ©e
- â Les indicateurs de chargement sont affichĂ©s pendant les requĂȘtes
- â Les codes de statut HTTP sont vĂ©rifiĂ©s
Commencez simple avec des requĂȘtes GET basiques, puis ajoutez progressivement la gestion d'erreurs, l'authentification, et l'organisation du code. Ne cherchez pas Ă tout faire parfaitement dĂšs le dĂ©but !