Aller au contenu
⬅️ Retour au sommaire ⬅️ Retour au Chapitre 4

✅ Corrigés — Journée 4

Exercice 4.1 — Premier appel GET (JSONPlaceholder)

Illustration du corrigé :

Illustration flutter avec api et retour JSON

Illustration flutter avec api et retour JSON

Illustration flutter avec api et retour JSON

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class Utilisateur {
  final int id;
  final String nom;
  final String email;

  Utilisateur({required this.id, required this.nom, required this.email});

  factory Utilisateur.fromJson(Map<String, dynamic> json) {
    return Utilisateur(id: json['id'], nom: json['name'], email: json['email']);
  }
}

Future<List<Utilisateur>> chargerUtilisateurs() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/users');
  final response = await http.get(url);

  if (response.statusCode == 200) {
    final List<dynamic> data = jsonDecode(response.body);
    return data.map((e) => Utilisateur.fromJson(e)).toList();
  }
  throw Exception('Erreur ${response.statusCode}');
}

class PageUtilisateurs extends StatefulWidget {
  const PageUtilisateurs({super.key});
  @override
  State<PageUtilisateurs> createState() => _PageUtilisateursState();
}

class _PageUtilisateursState extends State<PageUtilisateurs> {
  late Future<List<Utilisateur>> _future;

  @override
  void initState() {
    super.initState();
    _future = chargerUtilisateurs();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Utilisateurs (JSONPlaceholder)")),
      body: FutureBuilder<List<Utilisateur>>(
        future: _future,
        builder: (context, snapshot) {
          // Etat 1 : chargement
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          // Etat 2 : erreur
          if (snapshot.hasError) {
            return Center(child: Text("Erreur : ${snapshot.error}"));
          }
          // Etat 3 : succès
          final utilisateurs = snapshot.data!;
          return ListView.builder(
            itemCount: utilisateurs.length,
            itemBuilder: (context, index) => ListTile(
              leading: CircleAvatar(child: Text("${utilisateurs[index].id}")),
              title: Text(utilisateurs[index].nom),
              subtitle: Text(utilisateurs[index].email),
            ),
          );
        },
      ),
    );
  }
}

Exercice 4.2 — CRUD création + suppression

Illustration flutter jour 4 CRUD

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class Etudiant {
  final int? id;
  final String nom;
  final String email;

  Etudiant({this.id, required this.nom, required this.email});

  factory Etudiant.fromJson(Map<String, dynamic> json) =>
      Etudiant(id: json['id'], nom: json['nom'], email: json['email']);

  Map<String, dynamic> toJson() => {'nom': nom, 'email': email};
}

const String baseUrl = 'http://10.0.2.2:8080/api/etudiants'; // adapter selon l'environnement

class PageEtudiantsCrud extends StatefulWidget {
  const PageEtudiantsCrud({super.key});
  @override
  State<PageEtudiantsCrud> createState() => _PageEtudiantsCrudState();
}

class _PageEtudiantsCrudState extends State<PageEtudiantsCrud> {
  List<Etudiant> _etudiants = [];
  bool _chargement = true;
  String? _erreur;

  @override
  void initState() {
    super.initState();
    _charger();
  }

  Future<void> _charger() async {
    setState(() { _chargement = true; _erreur = null; });
    try {
      final response = await http.get(Uri.parse(baseUrl));
      if (response.statusCode != 200) throw Exception('Code ${response.statusCode}');
      final List<dynamic> data = jsonDecode(response.body);
      setState(() => _etudiants = data.map((e) => Etudiant.fromJson(e)).toList());
    } catch (e) {
      setState(() => _erreur = e.toString());
    } finally {
      setState(() => _chargement = false);
    }
  }

  Future<void> _creerEtudiant(String nom, String email) async {
    final response = await http.post(
      Uri.parse(baseUrl),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'nom': nom, 'email': email}),
    );
    if (response.statusCode == 201) {
      await _charger(); // rafraîchit la liste
    }
  }

  Future<void> _supprimerEtudiant(int id) async {
    final response = await http.delete(Uri.parse('$baseUrl/$id'));
    if (response.statusCode == 204) {
      await _charger();
    }
  }

  void _afficherFormulaireAjout() {
    final nomCtrl = TextEditingController();
    final emailCtrl = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("Nouvel étudiant"),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(controller: nomCtrl, decoration: const InputDecoration(labelText: "Nom")),
            TextField(controller: emailCtrl, decoration: const InputDecoration(labelText: "Email")),
          ],
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("Annuler")),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              _creerEtudiant(nomCtrl.text, emailCtrl.text);
            },
            child: const Text("Créer"),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Étudiants (CRUD)")),
      floatingActionButton: FloatingActionButton(
        onPressed: _afficherFormulaireAjout,
        child: const Icon(Icons.add),
      ),
      body: _chargement
          ? const Center(child: CircularProgressIndicator())
          : _erreur != null
              ? Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text("Erreur : $_erreur"),
                      ElevatedButton(onPressed: _charger, child: const Text("Réessayer")),
                    ],
                  ),
                )
              : RefreshIndicator(
                  onRefresh: _charger,
                  child: ListView.builder(
                    itemCount: _etudiants.length,
                    itemBuilder: (context, index) {
                      final e = _etudiants[index];
                      return ListTile(
                        title: Text(e.nom),
                        subtitle: Text(e.email),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete, color: Colors.red),
                          onPressed: () => _supprimerEtudiant(e.id!),
                        ),
                      );
                    },
                  ),
                ),
    );
  }
}

Exercice 4.3 — Gestion des erreurs réseau

// Extrait clé : gestion du timeout et des erreurs de connexion
Future<List<Etudiant>> chargerAvecGestionErreurs() async {
  try {
    final response = await http
        .get(Uri.parse(baseUrl))
        .timeout(const Duration(seconds: 5)); // évite une attente infinie

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      return data.map((e) => Etudiant.fromJson(e)).toList();
    } else {
      throw Exception('Le serveur a répondu avec le code ${response.statusCode}');
    }
  } on http.ClientException {
    throw Exception('Impossible de contacter le serveur. Vérifiez votre connexion.');
  } on FormatException {
    throw Exception('Réponse du serveur illisible (JSON invalide).');
  } catch (e) {
    throw Exception('Erreur inattendue : $e');
  }
}

// Dans le widget, le bloc catch de FutureBuilder / try-catch du _charger()
// affiche déjà ce message avec un bouton "Réessayer" (voir Exercice 4.2 ci-dessus) :
// Center(child: Column(children: [Text("Erreur : $_erreur"), ElevatedButton(onPressed: _charger, ...)]))

Points clés :


TP de synthèse — Gestion des étudiants

Architecture en couches complète. Structure de fichiers :

lib/
├── main.dart
├── models/etudiant.dart
├── services/etudiant_service.dart
├── providers/etudiant_provider.dart
└── screens/
    ├── page_liste_etudiants.dart
    └── page_formulaire_etudiant.dart

models/etudiant.dart

class Etudiant {
  final int? id;
  final String nom;
  final String email;
  final String promotion;

  Etudiant({this.id, required this.nom, required this.email, required this.promotion});

  factory Etudiant.fromJson(Map<String, dynamic> json) => Etudiant(
        id: json['id'],
        nom: json['nom'],
        email: json['email'],
        promotion: json['promotion'] ?? '',
      );

  Map<String, dynamic> toJson() => {'nom': nom, 'email': email, 'promotion': promotion};

  Etudiant copyWith({String? nom, String? email, String? promotion}) => Etudiant(
        id: id,
        nom: nom ?? this.nom,
        email: email ?? this.email,
        promotion: promotion ?? this.promotion,
      );
}

services/etudiant_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/etudiant.dart';

class EtudiantService {
  static const String _baseUrl = 'http://10.0.2.2:8080/api/etudiants'; // à adapter

  Future<List<Etudiant>> getTous() async {
    final response = await http.get(Uri.parse(_baseUrl)).timeout(const Duration(seconds: 5));
    if (response.statusCode != 200) throw Exception('Erreur serveur (${response.statusCode})');
    final List<dynamic> data = jsonDecode(response.body);
    return data.map((e) => Etudiant.fromJson(e)).toList();
  }

  Future<Etudiant> creer(Etudiant etudiant) async {
    final response = await http.post(
      Uri.parse(_baseUrl),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(etudiant.toJson()),
    );
    if (response.statusCode != 201) throw Exception('Erreur lors de la création');
    return Etudiant.fromJson(jsonDecode(response.body));
  }

  Future<Etudiant> mettreAJour(Etudiant etudiant) async {
    final response = await http.put(
      Uri.parse('$_baseUrl/${etudiant.id}'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(etudiant.toJson()),
    );
    if (response.statusCode != 200) throw Exception('Erreur lors de la mise à jour');
    return Etudiant.fromJson(jsonDecode(response.body));
  }

  Future<void> supprimer(int id) async {
    final response = await http.delete(Uri.parse('$_baseUrl/$id'));
    if (response.statusCode != 204) throw Exception('Erreur lors de la suppression');
  }
}

providers/etudiant_provider.dart

import 'package:flutter/foundation.dart';
import '../models/etudiant.dart';
import '../services/etudiant_service.dart';

class EtudiantProvider extends ChangeNotifier {
  final EtudiantService _service = EtudiantService();

  List<Etudiant> _etudiants = [];
  bool _chargement = false;
  String? _erreur;
  String? _filtrePromotion;

  bool get chargement => _chargement;
  String? get erreur => _erreur;
  String? get filtrePromotion => _filtrePromotion;

  List<Etudiant> get etudiants => _filtrePromotion == null
      ? _etudiants
      : _etudiants.where((e) => e.promotion == _filtrePromotion).toList();

  List<String> get promotionsDisponibles =>
      _etudiants.map((e) => e.promotion).toSet().toList()..sort();

  void filtrerParPromotion(String? promotion) {
    _filtrePromotion = promotion;
    notifyListeners();
  }

  Future<void> charger() async {
    _chargement = true;
    _erreur = null;
    notifyListeners();
    try {
      _etudiants = await _service.getTous();
    } catch (e) {
      _erreur = e.toString();
    } finally {
      _chargement = false;
      notifyListeners();
    }
  }

  Future<void> ajouter(Etudiant etudiant) async {
    final nouveau = await _service.creer(etudiant);
    _etudiants.add(nouveau);
    notifyListeners();
  }

  Future<void> modifier(Etudiant etudiant) async {
    final maj = await _service.mettreAJour(etudiant);
    final index = _etudiants.indexWhere((e) => e.id == maj.id);
    if (index != -1) _etudiants[index] = maj;
    notifyListeners();
  }

  Future<void> supprimer(int id) async {
    await _service.supprimer(id);
    _etudiants.removeWhere((e) => e.id == id);
    notifyListeners();
  }
}

screens/page_liste_etudiants.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/etudiant_provider.dart';
import 'page_formulaire_etudiant.dart';

class PageListeEtudiants extends StatefulWidget {
  const PageListeEtudiants({super.key});
  @override
  State<PageListeEtudiants> createState() => _PageListeEtudiantsState();
}

class _PageListeEtudiantsState extends State<PageListeEtudiants> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<EtudiantProvider>().charger();
    });
  }

  Future<void> _confirmerSuppression(BuildContext context, int id, String nom) async {
    final confirme = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("Confirmer la suppression"),
        content: Text("Supprimer $nom ?"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Annuler")),
          TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("Supprimer")),
        ],
      ),
    );
    if (confirme == true && context.mounted) {
      context.read<EtudiantProvider>().supprimer(id);
    }
  }

  @override
  Widget build(BuildContext context) {
    final provider = context.watch<EtudiantProvider>();

    return Scaffold(
      appBar: AppBar(
        title: const Text("Gestion des étudiants"),
        actions: [
          // Bonus : filtre par promotion
          if (provider.promotionsDisponibles.isNotEmpty)
            PopupMenuButton<String?>(
              icon: const Icon(Icons.filter_list),
              onSelected: provider.filtrerParPromotion,
              itemBuilder: (context) => [
                const PopupMenuItem(value: null, child: Text("Toutes les promotions")),
                ...provider.promotionsDisponibles
                    .map((p) => PopupMenuItem(value: p, child: Text(p))),
              ],
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(
          context,
          MaterialPageRoute(builder: (_) => const PageFormulaireEtudiant()),
        ),
        child: const Icon(Icons.add),
      ),
      body: _construireCorps(provider),
    );
  }

  Widget _construireCorps(EtudiantProvider provider) {
    if (provider.chargement) {
      return const Center(child: CircularProgressIndicator());
    }
    if (provider.erreur != null) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("Erreur : ${provider.erreur}", textAlign: TextAlign.center),
            const SizedBox(height: 12),
            ElevatedButton(onPressed: provider.charger, child: const Text("Réessayer")),
          ],
        ),
      );
    }
    if (provider.etudiants.isEmpty) {
      return const Center(child: Text("Aucun étudiant"));
    }
    return RefreshIndicator(
      onRefresh: provider.charger,
      child: ListView.builder(
        itemCount: provider.etudiants.length,
        itemBuilder: (context, index) {
          final etudiant = provider.etudiants[index];
          return Dismissible(
            key: ValueKey(etudiant.id),
            direction: DismissDirection.endToStart,
            background: Container(
              color: Colors.red,
              alignment: Alignment.centerRight,
              padding: const EdgeInsets.only(right: 20),
              child: const Icon(Icons.delete, color: Colors.white),
            ),
            confirmDismiss: (_) async {
              await _confirmerSuppression(context, etudiant.id!, etudiant.nom);
              return false; // on gère la suppression via le provider, pas via le swipe direct
            },
            child: ListTile(
              title: Text(etudiant.nom),
              subtitle: Text("${etudiant.email} · ${etudiant.promotion}"),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => PageFormulaireEtudiant(etudiant: etudiant)),
              ),
            ),
          );
        },
      ),
    );
  }
}

screens/page_formulaire_etudiant.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/etudiant.dart';
import '../providers/etudiant_provider.dart';

class PageFormulaireEtudiant extends StatefulWidget {
  final Etudiant? etudiant; // null = création, non-null = modification

  const PageFormulaireEtudiant({super.key, this.etudiant});

  @override
  State<PageFormulaireEtudiant> createState() => _PageFormulaireEtudiantState();
}

class _PageFormulaireEtudiantState extends State<PageFormulaireEtudiant> {
  final _formKey = GlobalKey<FormState>();
  late TextEditingController _nomCtrl;
  late TextEditingController _emailCtrl;
  late TextEditingController _promotionCtrl;
  bool _enregistrement = false;

  bool get _estModification => widget.etudiant != null;

  @override
  void initState() {
    super.initState();
    _nomCtrl = TextEditingController(text: widget.etudiant?.nom ?? '');
    _emailCtrl = TextEditingController(text: widget.etudiant?.email ?? '');
    _promotionCtrl = TextEditingController(text: widget.etudiant?.promotion ?? '');
  }

  @override
  void dispose() {
    _nomCtrl.dispose();
    _emailCtrl.dispose();
    _promotionCtrl.dispose();
    super.dispose();
  }

  Future<void> _enregistrer() async {
    if (!_formKey.currentState!.validate()) return;
    setState(() => _enregistrement = true);

    final etudiant = Etudiant(
      id: widget.etudiant?.id,
      nom: _nomCtrl.text.trim(),
      email: _emailCtrl.text.trim(),
      promotion: _promotionCtrl.text.trim(),
    );

    try {
      final provider = context.read<EtudiantProvider>();
      if (_estModification) {
        await provider.modifier(etudiant);
      } else {
        await provider.ajouter(etudiant);
      }
      if (mounted) Navigator.pop(context);
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur : $e")));
      }
    } finally {
      if (mounted) setState(() => _enregistrement = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(_estModification ? "Modifier l'étudiant" : "Nouvel étudiant")),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _nomCtrl,
                decoration: const InputDecoration(labelText: "Nom", border: OutlineInputBorder()),
                validator: (v) => (v == null || v.isEmpty) ? "Le nom est requis" : null,
              ),
              const SizedBox(height: 12),
              TextFormField(
                controller: _emailCtrl,
                decoration: const InputDecoration(labelText: "Email", border: OutlineInputBorder()),
                validator: (v) => (v == null || !v.contains('@')) ? "Email invalide" : null,
              ),
              const SizedBox(height: 12),
              TextFormField(
                controller: _promotionCtrl,
                decoration: const InputDecoration(labelText: "Promotion", border: OutlineInputBorder()),
                validator: (v) => (v == null || v.isEmpty) ? "La promotion est requise" : null,
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _enregistrement ? null : _enregistrer,
                  child: _enregistrement
                      ? const SizedBox(
                          height: 20, width: 20,
                          child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                        )
                      : const Text("Enregistrer"),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/etudiant_provider.dart';
import 'screens/page_liste_etudiants.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => EtudiantProvider(),
      child: const GestionEtudiantsApp(),
    ),
  );
}

class GestionEtudiantsApp extends StatelessWidget {
  const GestionEtudiantsApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gestion des Étudiants',
      theme: ThemeData(primarySwatch: Colors.teal, useMaterial3: true),
      home: const PageListeEtudiants(),
    );
  }
}

Points clés du corrigé complet :


⬅️ Retour au Chapitre 4 ➡️ Chapitre 5