Jetpack Compose : construire l’application de fitness

Kotlin
Jetpack Compose
Exercices
Atelier pratique Jetpack Compose : développer progressivement une application de fitness (catalogue, session active, historique, statistiques) avec Material Design 3, la gestion d’état et la remontée d’état.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2026-11-01

Objectifs

À la fin de cet atelier, vous saurez :

  • créer un projet Compose à partir du modèle Empty Activity (Compose) et le lancer sur un émulateur ;
  • composer une interface avec les composants Material Design 3 de base (Text, Button, Icon, Card, Scaffold, TopAppBar, FAB, LazyColumn) ;
  • gérer l’état d’une interface Compose (remember, MutableState) et déclencher des recompositions correctes ;
  • structurer une application multi-écrans par remontée d’état (hoisting) dans un composable racine ;
  • préparer le modèle de données qui alimentera la persistance Room du TP 08.

Ce TP est la mise en pratique du cours théorique Jetpack Compose (leçon 07). Les concepts n’y sont pas réexpliqués : en cas de doute, revenez au cours.

Brief de l’application (fil rouge)

Note

Créer une application de fitness simple permettant de suivre les exercices et les sessions d’entraînement. Dans un premier temps, vous vous concentrez sur l’interface utilisateur et les interactions de base.

Après les exercices de créativité réalisés en groupe, vous aurez une idée plus précise de l’application à développer.

Écrans à réaliser dans ce TP :

  • Catalogue d’exercices
  • Session d’entraînement active
  • Historique des sessions
  • Statistiques basiques

Le stockage des données et l’accès à des API distantes sont gérés plus tard (TP 08 pour le stockage local).

Modalités de réalisation et de validation

Cet atelier s’exécute hors du notebook : l’image de rendu du cours ne contient pas de SDK Android, aucun code Compose n’est exécuté ici. Trois canaux de validation se combinent :

  1. Réalisations externes : chaque exercice E1–E7 se fait dans Android Studio (projet Compose, émulateur AVD). Vous déposez une capture d’écran par exercice, nommée E1_accueil.png, E2_scaffold.png, etc.
  2. Quiz embarqués : les encadrés « Quiz » de ce fichier testent la compréhension du code (prédire une recomposition, repérer un piège d’ordre de Modifier, deviner ce qui s’affiche). Répondez avant de dérouler le corrigé.
  3. Auto-contrôles : chaque exercice se termine par une liste de faits observables à l’écran. Cochez-les sur votre émulateur avant de passer au suivant.

Les blocs de code Kotlin de ce fichier sont affichés, pas exécutés : les stubs (TODO) sont votre point de départ dans Android Studio, les corrigés indicatifs vous débloquent après tentative.

E0 — Mise en route

NoteTâches
  1. Dans Android Studio : New Project → Empty Activity (Compose), minSdk 24, langage Kotlin. Notez le nom du package retenu.
  2. Identifiez dans le projet généré : la classe Activity (avec setContent { ... }), le thème (MaterialTheme), et le composable de démonstration avec son @Preview.
  3. Créez un appareil virtuel (AVD, API 34 par exemple) et lancez l’application sur l’émulateur.
  4. Dans votre fichier principal, écrivez un premier composable personnel :
@Composable
fun MonPremierComposable(name: String) {
    Text(text = "Bonjour $name, prêt pour la séance ?")
}

@Preview(showBackground = true)
@Composable
fun MonPremierComposablePreview() {
    MaterialTheme {
        MonPremierComposable(name = "Coach")
    }
}
  1. Affichez MonPremierComposablePreview dans le volet Split (Design) et faites évoluer le paramètre name.

Auto-contrôle E0 (tous ces faits doivent être vrais sur votre poste) :

  • L’application démarre sur l’émulateur sans erreur dans la fenêtre Build.
  • Le volet Preview rend MonPremierComposable sans erreur rouge « Layoutlib » — ce message signale que le moteur de rendu de la preview n’a pas pu dessiner le composable (le volet reste gris ou vide).
  • Modifier name dans la preview change le texte affiché dans le volet Design après recomposition de la preview (bouton d’actualisation si besoin).

E1 — Premiers composables : écran d’accueil

NoteTâches
  1. Écrivez un composable paramétré GreetingCard(message: String) qui affiche un titre avec un Text stylé (taille et graisse via TextStyle).
  2. Écrivez l’écran AccueilScreen() : un titre, le GreetingCard, un Button « Démarrer une session » (avec un onClick qui fait un println("démarrer") pour l’instant) et un Icon évocateur (par exemple Icons.Default.FitnessCenter — dépendance material-icons-extended si nécessaire).
  3. Appelez AccueilScreen() dans le setContent de l’Activity, à l’intérieur de MaterialTheme.

Stub de départ :

@Composable
fun GreetingCard(message: String) {
    // TODO: un Text avec style (fontSize, fontWeight)
}

@Composable
fun AccueilScreen() {
    // TODO: titre + GreetingCard + Button("Démarrer une session") + Icon
}
@Composable
fun GreetingCard(message: String) {
    Text(
        text = message,
        style = TextStyle(
            fontSize = 22.sp,
            fontWeight = FontWeight.Bold
        )
    )
}

@Composable
fun AccueilScreen() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("FitTrack", style = MaterialTheme.typography.headlineMedium)
        GreetingCard(message = "Aujourd'hui : jambes et gainage")
        Button(onClick = { println("démarrer") }) {
            Text("Démarrer une session")
        }
        Icon(
            imageVector = Icons.Default.FitnessCenter,
            contentDescription = "Musculation"
        )
    }
}

Auto-contrôle E1 :

  • À l’exécution, l’écran montre le titre, le message, un bouton violet (thème MD3 par défaut ; il deviendra vert en E2) et une icône.
  • Un clic sur le bouton imprime démarrer dans la console Logcat.
  • La preview de AccueilScreen rend le même contenu que l’émulateur.

E2 — Scaffold et thème Material Design 3

NoteTâches
  1. Remplacez le Column brut de AccueilScreen par un Scaffold avec : une TopAppBar (titre « FitTrack »), un floatingActionButton (icône Icons.Default.Add), et un contenu centré.
  2. Fabriquez une colorScheme fitness : primary vert foncé (par ex. Color(0xFF2E7D32)), secondary orange, le reste aux valeurs par défaut ; appliquez-la via MaterialTheme(colorScheme = ...).
  3. Affichez la même phrase deux fois : une dans MaterialTheme(colorScheme = FitnessColorScheme), une dans un MaterialTheme par défaut ; repérez la différence de couleur.

Stub de départ :

// Dans l'application complète, le jeu clair/sombre est choisi via isSystemInDarkTheme (cours §9.1) ;
// le TP fixe un seul schéma pour toute la durée de l'atelier.
private val FitnessColorScheme = darkColorScheme(
    primary = Color(0xFF2E7D32),
    // TODO: secondary, etc.
)

@Composable
fun FitTrackApp() {
    MaterialTheme(colorScheme = FitnessColorScheme) {
        // TODO: Scaffold + TopAppBar + FAB + contenu
    }
}

Auto-contrôle E2 :

  • Une barre supérieure affiche « FitTrack » ; un bouton flottant rond apparaît en bas à droite.
  • Le FAB et le Button de l’accueil utilisent le vert défini dans FitnessColorScheme.
  • La même phrase, rendue une fois sous FitnessColorScheme et une fois sous thème par défaut, apparaît dans deux couleurs différentes : c’est l’effet visible du changement de colorScheme (tâche 3).
  • Le contenu défile sous la TopAppBar sans passer dessous (rôle de contentPadding du Scaffold).

E3 — Carte d’exercice : Card, Column/Row/Box et le piège des Modifiers

NoteTâches
  1. Définissez le modèle :
data class Exercise(
    val id: Long,
    val name: String,
    val sets: Int,
    val reps: Int,
    val weightKg: Double
)
  1. Écrivez ExerciseCard(exercise: Exercise, setIndex: Int = 1, modifier: Modifier = Modifier) : une Card contenant une Row — à gauche un Box rond affichant l’index de la série, à droite une Column (nom de l’exercice en gras, détail 3 x 12), et le poids dans un badge.
  2. Donnez à la Column de droite Modifier.weight(1f) pour qu’elle occupe l’espace restant.
ImportantQuiz 1 — ordre des Modifiers

Que peint l’instruction suivante, et pourquoi ?

Box(
    modifier = Modifier
        .padding(16.dp)
        .background(Color.Green)
) {
    Text("Série 1", modifier = Modifier.padding(8.dp))
}

Comparons avec :

Box(
    modifier = Modifier
        .background(Color.Green)
        .padding(16.dp)
) {
    Text("Série 1", modifier = Modifier.padding(8.dp))
}

Déroulez le corrigé après avoir répondu.

Les Modifier s’appliquent de l’extérieur vers l’intérieur, dans l’ordre d’écriture : le premier maillon écrit est posé en premier et chaque maillon suivant habille le résultat du précédent. Version A (padding puis background) : padding(16.dp) réserve d’abord 16 dp tout autour, puis background peint la surface restante — le vert ne déborde pas sur la marge. Version B (background puis padding) : le vert occupe d’abord tout l’espace disponible, puis le padding(16.dp) rentre le contenu à l’intérieur de la zone peinte — le fond envahit l’emplacement de la marge. Permuter deux maillons change donc le rendu : c’est le piège n°1 de mise en page Compose (cours §6.1).

@Composable
fun ExerciseCard(
    exercise: Exercise,
    setIndex: Int = 1,                      // numéro de la série représentée par la carte
    modifier: Modifier = Modifier
) {
    Card(modifier = modifier.fillMaxWidth().padding(vertical = 4.dp)) {
        Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Box(                              // le Box rond : l'INDEX de la série
                modifier = Modifier
                    .size(40.dp)
                    .background(MaterialTheme.colorScheme.primary, CircleShape),
                contentAlignment = Alignment.Center
            ) {
                Text("$setIndex", color = Color.White)
            }
            Column(modifier = Modifier.weight(1f).padding(horizontal = 12.dp)) {
                Text(exercise.name, fontWeight = FontWeight.Bold)
                Text("${exercise.sets} x ${exercise.reps}")
            }
            Box(                              // le poids dans un badge
                modifier = Modifier
                    .background(MaterialTheme.colorScheme.secondaryContainer, RoundedCornerShape(8.dp))
                    .padding(horizontal = 8.dp, vertical = 2.dp)
            ) {
                Text("${exercise.weightKg} kg", style = MaterialTheme.typography.labelLarge)
            }
        }
    }
}

Auto-contrôle E3 :

  • La carte occupe toute la largeur, avec un rond coloré à gauche contenant l’index de la série.
  • Le texte du détail s’étend sur la largeur restante (le weight(1f) fonctionne) ; sans lui, la Column se plaque à gauche.
  • En permutant padding et background sur le Box rond, le cercle disparaît partiellement ou change de taille : vous avez observé le piège du quiz 1.

E4 — État et interactions : compteur de répétitions et validation de série

NoteTâches
  1. Dans ExerciseCard, rendez la carte interactive : deux boutons + et - ajustent le compteur de répétitions affiché (remember { mutableStateOf(exercise.reps) }).
  2. Ajoutez un Switch (ou un second bouton) pour marquer la série comme terminée ; le nom de l’exercice passe alors barré (TextDecoration.LineThrough).
  3. À chaque validation de série, affichez un Snackbar « Série validée » via le snackbarHost du Scaffold (passez un callback onSetDone: () -> Unit à la carte — préfiguration de la remontée d’état de E6).

Stub de départ :

@Composable
fun ExerciseCard(                       // la carte de E3, remplacée par une version interactive
    exercise: Exercise,
    onSetDone: () -> Unit
) {
    var reps by remember { mutableStateOf(exercise.reps) }   // mutableIntStateOf : variante Int équivalente (cours §4.2)
    var done by remember { mutableStateOf(false) }
    // TODO: Row avec Text(exercise.name), Button("-"), Button("+"), Switch(done)
    // TODO: onClick des boutons = { reps = (reps +/- 1).coerceAtLeast(0) }
}
ImportantQuiz 2 — prédire la recomposition
@Composable
fun SessionCounter() {
    var reps by remember { mutableIntStateOf(10) }
    RepsDisplay(reps)
    Button(onClick = { reps++ }) { Text("+") }
}

@Composable
fun RepsDisplay(reps: Int) {
    Text("Répétitions : $reps")
}

Un clic sur + : quelles fonctions composables sont recomposées ?

    1. SessionCounter uniquement
    1. SessionCounter et RepsDisplay
    1. RepsDisplay uniquement
    1. tout l’arbre de l’Activity

b) : SessionCounter lit reps, son corps est donc invalidé ; RepsDisplay reçoit une nouvelle valeur de paramètre, elle est donc recomposée aussi. Nuance : Compose peut sauter la recomposition d’un composable dont aucun paramètre ne change et qui ne lit pas l’état muté — ici le paramètre change, RepsDisplay est donc bien recomposée. Le bouton +, lui, ne change pas d’état : Compose recompose au niveau de granularité le plus fin possible les composables dont l’entrée (état lu ou paramètre) a changé — pas tout l’écran. Retenez la règle : lire un State dans un composable lie ce composable à sa recomposition.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExerciseCard(                       // la carte de E3, version interactive
    exercise: Exercise,
    onSetDone: () -> Unit
) {
    var reps by remember { mutableStateOf(exercise.reps) }
    var done by remember { mutableStateOf(false) }

    Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
        Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    exercise.name,
                    fontWeight = FontWeight.Bold,
                    textDecoration = if (done) TextDecoration.LineThrough else null
                )
                Text("$reps répétitions")
            }
            Row {
                OutlinedButton(onClick = { reps = (reps - 1).coerceAtLeast(0) }) { Text("-") }
                Spacer(Modifier.width(8.dp))
                OutlinedButton(onClick = { reps++ }) { Text("+") }
                Spacer(Modifier.width(8.dp))
                Switch(
                    checked = done,
                    onCheckedChange = { checked ->
                        done = checked
                        if (checked) onSetDone()
                    }
                )
            }
        }
    }
}

Côté Scaffold :

val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()  // ligne à copier telle quelle ; les coroutines ne sont pas au programme de ce cours
Scaffold(
    topBar = { /* ... */ },
    snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
    // modifier = Modifier.padding(padding) sur le contenu
    // onClick FAB -> scope.launch { snackbarHostState.showSnackbar("Série validée") }
}

Auto-contrôle E4 :

  • Les boutons +/- font varier le nombre affiché immédiatement.
  • Cocher le Switch barre le nom de l’exercice.
  • Valider une série fait apparaître un Snackbar « Série validée » en bas de l’écran, qui disparaît seul.
  • Après un aller-retour Catalogue → Session → Catalogue, le compteur revient à sa valeur initiale — normal, l’état remember meurt avec le retrait de l’arbre ; E6 le corrigera (à vérifier dès E6, quand la navigation existe).

E5 — Catalogue : LazyColumn, clés et en-tête

NoteTâches
  1. Construisez une liste de 8 exercices minimum : val catalogue = listOf(Exercise(1, "Squat", ...), ...).
  2. Affichez-la dans un LazyColumn avec : un item { } d’en-tête (« Catalogue — N exercices »), puis items(catalogue, key = { it.id }) { ex -> ExerciseCard(exercise = ex, onSetDone = { /* branché en E6 */ }) }.
  3. Ajoutez un second item de pied de liste affichant un résumé (nombre total de séries).
  4. Scrollez : vérifiez la fluidité et observez dans le Layout Inspector (ou via un println dans un composable d’en-tête de ligne) que seules les lignes visibles sont composées.
ImportantQuiz 3 — ce qui s’affiche
LazyColumn {
    item { Text("En-tête") }
    items(catalogue) { ex -> Text(ex.name) }
    items(catalogue.take(2)) { ex -> Text(ex.name) }
}

catalogue contient 8 exercices distincts. Combien de Text d’exercices sont affichés, et y a-t-il un problème ?

    1. 8, car Compose déduplique les exercices affichés deux fois
    1. 10, sans crash ; mais l’identité des lignes est positionnelle : si la liste est réordonnée, l’état remember d’une ligne suit la mauvaise ligne
    1. 10, avec un crash en programmation à cause de clés dupliquées
    1. 2, car le second bloc items écrase le premier

b) : ce code rend bien les 10 lignes sans crash. Sans key = explicite, rien ne peut collisionner : chaque ligne est simplement identifiée par sa position dans la portée du bloc. La fragilité est ailleurs, et c’est le point du cours §8.2 — sans clé, l’état reste attaché à la position : si la liste est réordonnée ou filtrée, les états remember des lignes (compteur de E4, panneau déroulé) suivent la mauvaise ligne. Un key explicite n’est donc nécessaire — et ne peut collisionner — qu’en cas de clés dupliquées entre lignes visibles. Règle : une clé stable et unique par élément dès que l’ordre de la liste peut varier.

Auto-contrôle E5 :

  • L’en-tête « Catalogue — 8 exercices » apparaît au-dessus des cartes, le pied de liste en dessous, après scroll complet.
  • Le scroll reste fluide avec 8+ cartes ; les ExerciseCard de E3/E4 sont réutilisées telles quelles dans la liste.
  • Sur une ligne, le compteur de répétitions est indépendant de celui de sa voisine (état local par item).

E6 — Structurer : quatre écrans par remontée d’état

NoteTâches
  1. Définissez les quatre écrans comme composables autonomes, chacun recevant ses données et ses callbacks en paramètres : CatalogueScreen(...), SessionScreen(...), HistoriqueScreen(...), StatistiquesScreen(...).
  2. Dans le composable racine FitTrackApp, gardez un seul état d’application : l’écran courant (var screen by remember { mutableStateOf(Screen.CATALOGUE) } avec un enum class Screen { CATALOGUE, SESSION, HISTORIQUE, STATISTIQUES }) et la liste des séries validées.
  3. Sélectionnez l’écran affiché avec un when (screen) ; changez d’écran via la TopAppBar (menu) ou une rangée de FilterChips.
  4. L’écran HistoriqueScreen affiche une liste statique d’entrées de démonstration pour l’instant (la vraie source de données vient en TP 08).
  5. Écrivez les statistiques basiques comme fonctions pures hors de l’UI, calculées sur la liste des enregistrements d’entraînement (le type Workout — champs durationMinutes, caloriesBurned, timestamp — est formalisé en E7 : posez-le dès maintenant si vous traitez E6 avant) :
fun totalCalories(workouts: List<Workout>): Int      // somme des caloriesBurned
fun averageDuration(workouts: List<Workout>): Double // moyenne des durationMinutes
fun bestStreak(workouts: List<Workout>): Int         // jours consécutifs via timestamp

et affichez leurs résultats dans StatistiquesScreen.

Avertissement

Pas de librairie de navigation. L’ajout d’écrans multiples se fait ici uniquement par état (when sur un enum). La librairie Navigation de Jetpack (routes, back stack, arguments typés) est hors périmètre de ce TP ; elle est abordée plus loin dans le cursus. Ne l’installez pas pour cet exercice.

Stub de départ :

enum class Screen { CATALOGUE, SESSION, HISTORIQUE, STATISTIQUES }

@Composable
fun FitTrackApp() {
    var screen by remember { mutableStateOf(Screen.CATALOGUE) }
    val validatedSets = remember { mutableStateListOf<Exercise>() }

    MaterialTheme(colorScheme = FitnessColorScheme) {
        Scaffold(
            topBar = { /* TODO: TopAppBar + navigation par état */ },
            snackbarHost = { /* TODO: depuis E4 */ }
        ) { padding ->
            when (screen) {
                Screen.CATALOGUE -> CatalogueScreen(
                    exercises = catalogue,
                    onStartSession = { /* TODO: screen = Screen.SESSION */ },
                    modifier = Modifier.padding(padding)
                )
                // TODO: les trois autres branches
            }
        }
    }
}
ImportantQuiz 4 — où placer l’état

Le compteur de répétitions de E4 est un remember local à la ligne de catalogue. L’utilisateur incrémente + trois fois sur « Squat », va à l’écran Session, puis revient au Catalogue : le compteur affiche 12 (sa valeur de départ) au lieu de 15. Pourquoi, et quelle est la correction conforme à la remontée d’état ?

    1. Bug de LazyColumn : il faut désactiver le recyclage
    1. La ligne a quitté l’arbre de composition : son remember local est détruit ; il faut hoister le compteur dans FitTrackApp (état par exerciseId dans une map) et le redescendre en paramètre
    1. Il faut remplacer remember par rememberSaveable et le problème disparaît sans changement d’architecture

b) est la correction du TP. Les leurres : l’option (a) est une fausse piste — le recyclage est une force de LazyColumn, compensée par des clés stables (quiz 3) ; et l’option (c) n’est vraie qu’en apparence, car rememberSaveable survit aux reconfigurations, pas au retrait de l’arbre de composition — et même si c’était le cas, un état partagé entre écrans doit vivre au niveau le plus proche de son consommateur commun : ici le composable racine. C’est la définition de la remontée d’état : l’état remonte à FitTrackApp, les écrans le reçoivent en paramètre et émettent des événements par callback.

ImportantQuiz 5 — statut de la source de vérité

StatistiquesScreen doit afficher « Séries validées : N ». Deux implémentations circulent dans le groupe :

  • version 1 : chaque ligne de catalogue compte ses validations dans un var local et StatistiquesScreen recopie le total dans un fichier shared ;
  • version 2 : FitTrackApp tient validatedSets (unique) ; CatalogueScreen appelle onSetDone(exercise) ; StatistiquesScreen(validatedSets) appelle les fonctions pures.

Laquelle respecte « une source de vérité unique », et qu’affiche la version 1 quand on valide une série puis change d’écran avant de regarder les statistiques ?

Version 2. Dans la version 1, l’état meurt avec chaque ligne recomposée ou retirée de l’arbre : le compteur global recopié est obsolète ou remis à zéro — un cas d’école de désynchronisation par duplication d’état. La version 2 centralise : événement qui remonte (onSetDone), état qui redescend (validatedSets), calcul sans effet de bord (fonctions pures totalCalories (qui opèrent sur la liste Workout de E7, posée cours §10.2), etc.) donc testable hors UI.

Auto-contrôle E6 :

  • Les quatre écrans sont atteignables depuis l’interface (chips ou menu) sans crash ni Activity supplémentaire.
  • Le compteur d’une ligne « Squat » survit à un aller-retour Catalogue → Session → Catalogue (état remonté).
  • Valider une série sur le catalogue change immédiatement le « N » de l’écran Statistiques.
  • L’écran Historique affiche ses données de démonstration statiques.
  • Les fonctions de statistique sont appelées avec des listes factices dans le main/les previews et renvoient les valeurs attendues.

E7 (facultatif — mais noté s’il est traité) — Pont vers le TP 08 : le modèle Workout

NoteTâches
  1. Formalisez la donnée persistée du TP 08 dès maintenant. Les champs exigés par l’exercice Room de 08_P sont : id, nom de l’exercice, séries, répétitions, poids, date, durée, calories brûlées. Écrivez la data class champ pour champ :
data class Workout(
    val id: Long = 0L,                  // 0 = non persisté (Room générera)
    val exerciseName: String,           // nom de l'exercice
    val sets: Int,                      // séries
    val reps: Int,                      // répétitions
    val weightKg: Double,               // poids
    val timestamp: Long,                // date (epoch millis)
    val durationMinutes: Int,           // durée
    val caloriesBurned: Int             // calories brûlées
)
  1. Faites consommer cette classe par E6 : une validation de série produit un Workout ; l’historique et les statistiques de l’app (E6) calculent sur List<Workout>.
  2. Remplacez la liste statique d’HistoriqueScreen par la liste des Workout validés pendant la session en cours (elle reste en mémoire : c’est attendu).
ImportantPont explicite vers le TP 08

Vos données E7 seront l’entité Room du TP 08. La data class ci-dessus est conçue pour être annotée @Entity telle quelle en TP 08 : mêmes champs, mêmes types (Long pour la clé et la date, aucun TypeConverter nécessaire au départ). Ne renommez aucun champ d’ici là. Livrables notés pour E7 : la capture d’écran de l’écran final affichant la liste des Workout de la session (E7_workout.png) et le dépôt du fichier Workout.kt dans le relevé de votre dépôt de cours — vous le réutiliserez tel quel en TP 08.

Auto-contrôle E7 :

  • Valider une série en Session crée un Workout (observé via Log.d("Workout", workout.toString())).
  • L’écran Historique affiche les Workout de la session courante, les plus récents en premier.
  • totalCalories et averageDuration sont calculés sur cette même liste (plus de liste factice séparée).

Grille de conformité (contrat de notation APC)

Critère Intitulé Exercices qui le couvrent Preuve déposée
C1 Composants Material E1, E2, E3, E5 captures E1_accueil.png, E2_scaffold.png, E3_carte.png, E5_catalogue.png, réponses aux quiz 1 et 3
C2 État et événements E4, E6 capture E4_etat.png (compteur + Snackbar), capture E6_etat_hoiste.png (compteur conservé après changement d’écran), réponses aux quiz 2 et 4
C3 Structure en 4 écrans E6 (+ E7) captures des 4 écrans E6_catalogue/session/historique/statistiques.png, réponse au quiz 5

Barème de conformité : un critère est validé si (a) les captures correspondantes montrent les faits d’interface exigés, (b) la liste d’auto-contrôle de chaque exercice couvert est entièrement vraie, (c) les quiz associés sont justifiés (la réponse seule ne suffit pas, l’explication compte).

Pour aller plus loin (hors périmètre, ne pas traiter)

Gestion des effets (LaunchedEffect), animations, ViewModel et cycle de vie, librairie Navigation, thèmes dynamiques Material You, persistance et API distantes : tous planifiés dans les TP suivants (08 et suite). Les mentions ci-dessus servent uniquement à reconnaître les pièges d’architecture, pas à être implémentées.

Réutilisation