| 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 |
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.
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.
- Crée
flutter create fioul_app(dossiermobile/du dépôt). - Reproduis la même arborescence (
models,services,providers,screens). - Ajoute
httpetproviderdanspubspec.yaml. - Définis une constante
apiBaseUrlvers ton API Spring.
http://10.0.2.2:8080 (et non localhost).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.
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(), ); }
- Crée la classe
Campagnequi reflète tonCampagneDTO. - Champs :
id,zone,statut,volumeTotal,prixLitre. - Écris
Campagne.fromJson()(attention aux nombres :as num). - Crée aussi un modèle
Commandepour le formulaire.
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.
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(); } }
- Crée
CampagneServiceavecgetOuvertes({String? zone}). - Construis l'URL avec le filtre :
/campagnes?zone=Ladignac. - Ajoute
getDetail(String id). - Prépare
rejoindre(id, Commande)enhttp.post(utilisé à l'étape 6).
L'écran liste
Afficher la liste renvoyée par l'API avec FutureBuilder (gestion du chargement/erreur) et ListView.builder.
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}'),
),
);
},
)
- Crée
CampagnesScreenavec unFutureBuilder. - Chaque item : une
Cardavec lazone, levolumeTotalet leprixLitre. - Gère l'état de chargement et l'état d'erreur (message clair).
- Ajoute un « pull-to-refresh » (
RefreshIndicator).
LinearProgressIndicator).L'écran détail & la navigation
Passer d'un écran à l'autre en transmettant un identifiant, puis charger le détail depuis l'API.
// depuis la liste : onTap: () => Navigator.push(ctx, MaterialPageRoute( builder: (_) => LivreDetail(id: livres[i].id), )); // dans LivreDetail : charger le détail + afficher les avis
- 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).
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.
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 } }
- 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
ChangeNotifierProvideren haut de l'app. - Consomme-le dans les écrans avec
Consumer<CampagneProvider>.
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.
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... }
- Formulaire :
litres(nombre),commune,adresse. - Validation client : litres ≥ 500 (aligné sur la règle serveur).
- À la soumission :
POST /campagnes/{id}/commandes. - Affiche un
SnackBarde succès, ou l'erreur renvoyée par l'API (ex. campagne clôturée). - Rafraîchis le détail (volume + prix mis à jour).
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.
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'}
- É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.