↑
CHAPITRE 5.4

Connexion aux Web Services et API REST

Communiquer avec des serveurs distants et récupérer des données depuis Internet dans votre application Flutter
Dans cette section, vous allez dĂ©couvrir comment connecter votre application Flutter Ă  des services web et des API REST. Vous apprendrez Ă  effectuer des requĂȘtes HTTP pour rĂ©cupĂ©rer et envoyer des donnĂ©es, Ă  traiter les rĂ©ponses JSON, et Ă  gĂ©rer les erreurs rĂ©seau. C'est la base pour crĂ©er des applications qui interagissent avec des serveurs distants. 🌐

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

Analogie simple :
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)
L'API REST fonctionne de la mĂȘme maniĂšre, mais avec des donnĂ©es sur Internet.

📡 Les mĂ©thodes HTTP

Les API REST utilisent des méthodes HTTP standard pour indiquer l'action à effectuer :

Méthode
Action
Exemple
GET
Récupérer des données
Obtenir la liste des utilisateurs
POST
Créer de nouvelles données
Ajouter un nouvel utilisateur
PUT
Mettre à jour des données
Modifier les informations d'un utilisateur
DELETE
Supprimer des données
Supprimer un utilisateur

🔗 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"
  }
]
JSON en Dart :
En Dart, JSON est représenté comme :
  • Objet JSON → Map<String, dynamic>
  • Tableau JSON → List<dynamic> ou List<Map<String, dynamic>>
Vous pouvez facilement convertir entre JSON et objets Dart avec 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
💡 API de test gratuite :
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
Ces API sont parfaites pour apprendre sans avoir besoin de créer votre propre serveur !

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 :
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
Version du package
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';
Pourquoi 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

Pour vérifier que le package est correctement installé, créez un fichier de test :
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>
Configuration iOS :
Sur iOS, aucune configuration supplémentaire n'est nécessaire. L'accÚs à Internet est autorisé par défaut.
💡 Astuce
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)
Codes de statut HTTP courants :
  • 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() :
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}');
  }
}
Important :
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,
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}
Content-Type :
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.
Important :
  • POST et PUT nĂ©cessitent un body avec 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 statusCode pour 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

Méthode
Description
Exemple
json.decode()
Convertit une chaĂźne JSON en objet Dart
json.decode('{"nom": "Jean"}')
json.encode()
Convertit un objet Dart en chaĂźne JSON
json.encode({'nom': 'Jean'})
jsonEncode()
Alias de json.encode()
jsonEncode({'nom': 'Jean'})
jsonDecode()
Alias de json.decode()
jsonDecode('{"nom": "Jean"}')
fromJson et toJson :
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.
Gestion des types :
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)
Timeout :
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.
Important :
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

Header
Description
Exemple
Content-Type
Type de contenu envoyé
'Content-Type': 'application/json'
Authorization
Token d'authentification
'Authorization': 'Bearer token123'
Accept
Type de contenu accepté
'Accept': 'application/json'
X-API-Key
Clé API personnalisée
'X-API-Key': 'ma_cle'
Bearer Token :
"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.
Sécurité :
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.

Analogie :
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
C'est exactement comme ça que fonctionnent les requĂȘtes rĂ©seau avec async/await !

📝 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

🎯 Objectif de l'exercice :
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
);
Structure de la table :
  • 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']);
}
?>
Points importants :
  • CORS : Les headers Access-Control-Allow-Origin permettent Ă  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)
⚠ Important : Configuration de l'adresse IP
Pour que votre application Flutter puisse accéder à l'API sur votre ordinateur :
  1. Trouver votre adresse IP locale :
    • Windows : Ouvrez CMD et tapez ipconfig, cherchez "IPv4 Address"
    • Mac/Linux : Ouvrez Terminal et tapez ifconfig ou ip addr
  2. Modifier le code Flutter : Remplacez 192.168.1.100 par votre adresse IP réelle
  3. Vérifier le firewall : Assurez-vous que le port 80 (Apache) n'est pas bloqué
  4. Pour Android : Utilisez 10.0.2.2 si 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
🔐 SĂ©curitĂ© recommandĂ©e :
  • 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() et password_verify()
  • PrĂ©paration des requĂȘtes : Toujours utiliser prepare() pour Ă©viter les injections SQL
  • Sanitization : Utilisez htmlspecialchars() et filter_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
}
💡 Pourquoi ces amĂ©liorations ?
  • 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)
🎯 Cet exemple couvre 80% des besoins rĂ©els :
  • ✅ Structure de base de donnĂ©es
  • ✅ API REST (GET, POST)
  • ✅ Authentification basique
  • ✅ Validation des donnĂ©es
  • ✅ Gestion d'erreurs
  • ✅ Interface Flutter complĂšte
Les 20% restants incluent : PUT/DELETE, pagination, recherche, upload de fichiers, notifications push, etc.

📋 Checklist des bonnes pratiques

Checklist avant de déployer :
  • ✅ 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
💡 Conseil final :
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 !