🛢️Parcours Fioul GroupéGuide pas à pas — Flutter
Accueil › Parcours 3 · Flutter
📱 Parcours 3 — Application mobile

Construire l'appli mobile, écran par écran

Même démarche en couches, côté client : modèle → service HTTP → liste → détail → état → formulaire → authentification. À chaque étape, l'exemple médiathèque puis l'adaptation au fioul. L'appli consomme la vraie API du parcours 2.

1
J'observe l'écranCode Dart/Flutter commenté sur l'exemple « Livre ».
2
Je fais le mienLe même écran pour les campagnes de fioul.
La symétrie avec l'API — chaque couche Flutter reflète une couche Spring :
Côté Spring (parcours 2)Côté Flutter (ici)
DTO (JSON)↔Modèle Dart + fromJson
Endpoints REST↔Service HTTP (http.get/post)
GET /campagnes↔Écran liste
POST /commandes↔Formulaire d'inscription
0 Étape 0

Créer le projet & l'organiser

Un projet Flutter avec une structure de dossiers claire : on sépare modèles, services et écrans dès le départ.

Exemple Arborescence
flutter create mediatheque_app

lib/
├── models/      // classes de données
├── services/    // appels HTTP
├── providers/   // état
├── screens/     // les écrans
└── main.dart

Dans pubspec.yaml, on ajoute les paquets http et provider.

À toi — Fioul Le projet mobile
  • Crée flutter create fioul_app (dossier mobile/ du dépôt).
  • Reproduis la même arborescence (models, services, providers, screens).
  • Ajoute http et provider dans pubspec.yaml.
  • Définis une constante apiBaseUrl vers ton API Spring.
Émulateur Android — pour joindre une API locale, utilise http://10.0.2.2:8080 (et non localhost).
1 Étape 1

Le modèle Dart

Une classe qui reflète le DTO renvoyé par l'API, avec un constructeur fromJson pour transformer la réponse JSON en objet Dart.

Exemple Livre
class Livre {
  final String id;
  final String titre;
  final double noteMoyenne;

  Livre({required this.id,
        required this.titre,
        required this.noteMoyenne});

  factory Livre.fromJson(Map<String, dynamic> j) => Livre(
    id: j['id'],
    titre: j['titre'],
    noteMoyenne: (j['noteMoyenne'] as num).toDouble(),
  );
}
À toi — Fioul Campagne
  • Crée la classe Campagne qui reflète ton CampagneDTO.
  • Champs : id, zone, statut, volumeTotal, prixLitre.
  • Écris Campagne.fromJson() (attention aux nombres : as num).
  • Crée aussi un modèle Commande pour le formulaire.
Astuce — le nom des champs JSON doit correspondre exactement à ceux de ton DTO Spring. En cas de doute, regarde la réponse dans Swagger.
2 Étape 2

Le service HTTP

Une classe qui appelle l'API et renvoie des objets Dart. C'est le pendant du contrôleur Spring, vu du client.

Exemple LivreService
class LivreService {
  static const _base = 'http://10.0.2.2:8080/api';

  Future<List<Livre>> getAll() async {
    final r = await http.get(Uri.parse('$_base/livres'));
    if (r.statusCode != 200) throw Exception('Erreur API');
    final List data = jsonDecode(r.body);
    return data.map((j) => Livre.fromJson(j)).toList();
  }
}
À toi — Fioul CampagneService
  • Crée CampagneService avec getOuvertes({String? zone}).
  • Construis l'URL avec le filtre : /campagnes?zone=Ladignac.
  • Ajoute getDetail(String id).
  • Prépare rejoindre(id, Commande) en http.post (utilisé à l'étape 6).
Bon réflexe — centralise la gestion d'erreurs ici (codes 400/401/409 renvoyés par l'API) plutôt que dans chaque écran.
3 Étape 3

L'écran liste

Afficher la liste renvoyée par l'API avec FutureBuilder (gestion du chargement/erreur) et ListView.builder.

Exemple Liste des livres
FutureBuilder<List<Livre>>(
  future: LivreService().getAll(),
  builder: (ctx, snap) {
    if (!snap.hasData) return CircularProgressIndicator();
    final livres = snap.data!;
    return ListView.builder(
      itemCount: livres.length,
      itemBuilder: (_, i) => ListTile(
        title: Text(livres[i].titre),
        trailing: Text('★ ${livres[i].noteMoyenne}'),
      ),
    );
  },
)
À toi — Fioul Liste des campagnes
  • Crée CampagnesScreen avec un FutureBuilder.
  • Chaque item : une Card avec la zone, le volumeTotal et le prixLitre.
  • Gère l'état de chargement et l'état d'erreur (message clair).
  • Ajoute un « pull-to-refresh » (RefreshIndicator).
Touche produit — affiche une petite jauge de progression du volume vers le prochain palier (widget LinearProgressIndicator).
4 Étape 4

L'écran détail & la navigation

Passer d'un écran à l'autre en transmettant un identifiant, puis charger le détail depuis l'API.

Exemple Détail d'un livre
// depuis la liste :
onTap: () => Navigator.push(ctx, MaterialPageRoute(
  builder: (_) => LivreDetail(id: livres[i].id),
));

// dans LivreDetail : charger le détail + afficher les avis
À toi — Fioul Détail d'une campagne
  • Au tap sur une campagne, navigue vers CampagneDetail(id).
  • Charge le détail (getDetail) et affiche zone, date limite, volume, prix.
  • Affiche la jauge volume → palier, plus grande.
  • Ajoute un bouton « Rejoindre cette campagne » (mène au formulaire de l'étape 6).
5 Étape 5

La gestion d'état avec Provider

Partager les données entre écrans et rafraîchir l'UI automatiquement, sans tout recharger à la main, grâce à ChangeNotifier.

Exemple LivreProvider
class LivreProvider extends ChangeNotifier {
  List<Livre> _livres = [];
  List<Livre> get livres => _livres;

  Future<void> charger() async {
    _livres = await LivreService().getAll();
    notifyListeners();   // l'UI se met à jour
  }
}
À toi — Fioul CampagneProvider
  • Crée CampagneProvider extends ChangeNotifier.
  • Méthodes : charger(zone), rejoindre(...).
  • Après une inscription, appelle notifyListeners() pour rafraîchir la jauge.
  • Enregistre le provider avec ChangeNotifierProvider en haut de l'app.
  • Consomme-le dans les écrans avec Consumer<CampagneProvider>.
6 Étape 6

Le formulaire & l'envoi (POST)

Saisir des données validées et les envoyer à l'API. C'est le miroir de la validation Spring de l'étape 6 côté serveur.

Exemple Ajouter un avis
final _form = GlobalKey<FormState>();

TextFormField(
  decoration: InputDecoration(labelText: 'Note /5'),
  keyboardType: TextInputType.number,
  validator: (v) {
    final n = int.tryParse(v ?? '');
    if (n == null || n < 0 || n > 5) return 'Note entre 0 et 5';
    return null;
  },
)
// à la validation : if (_form.currentState!.validate()) { ...post... }
À toi — Fioul S'inscrire à une campagne
  • Formulaire : litres (nombre), commune, adresse.
  • Validation client : litres ≥ 500 (aligné sur la règle serveur).
  • À la soumission : POST /campagnes/{id}/commandes.
  • Affiche un SnackBar de succès, ou l'erreur renvoyée par l'API (ex. campagne clôturée).
  • Rafraîchis le détail (volume + prix mis à jour).
Double validation — le client valide pour le confort, le serveur valide pour la sécurité. Les deux sont nécessaires.
7 Étape 7

L'authentification JWT

Se connecter, stocker le jeton, et l'envoyer dans l'en-tête Authorization des requêtes protégées. Le pendant client de la sécurité Spring.

Exemple Connexion & jeton
Future<String> login(String email, String mdp) async {
  final r = await http.post(
    Uri.parse('$_base/auth/login'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'email': email, 'motDePasse': mdp}),
  );
  return jsonDecode(r.body)['token'];   // le JWT
}

// requête protégée :
headers: {'Authorization': 'Bearer $token'}
À toi — Fioul Connexion des utilisateurs
  • Écran de connexion / inscription (particulier, coordinateur).
  • Stocke le jeton (paquet flutter_secure_storage).
  • Ajoute le header Authorization à toutes les requêtes protégées.
  • Réserve « ouvrir une campagne » au rôle coordinateur.
Passerelle Cyber — ne stocke jamais le jeton en clair ; gère son expiration ; ne fais pas confiance au rôle côté client (le serveur reste l'autorité).
Bilan du parcours — l'appli mobile consomme l'API sécurisée, de bout en bout : MongoDB → Spring → Flutter. Le produit Fioul Groupé est complet.
← Parcours 2 · API Spring