← Retour au cours
▶ Aperçu gratuit · Leçon offerte

Jetpack Compose : UI déclarative, state, side effects, Navigation, Material 3

⏱ 840 min · 🎬 Lecon · 🏆 20 XP
🎬
Vidéo en production
Notre équipe pédagogique tourne actuellement cette leçon avec un·e formateur·rice expert·e. Le contenu textuel ci-dessous est complet et utilisable dès maintenant.

Leçon 2 — Jetpack Compose : UI déclarative moderne

Composables, state, side effects, Navigation, Material 3 : construire des interfaces réactives en Kotlin pur.

🎯 Objectifs pédagogiques

  • Comprendre le modèle déclaratif de Jetpack Compose et la recomposition.
  • Gérer l'état d'une UI avec remember, mutableStateOf, rememberSaveable.
  • Utiliser les side effects (LaunchedEffect, DisposableEffect, derivedStateOf, produceState).
  • Construire une navigation typée avec Navigation Compose et NavHost.
  • Appliquer le design system Material 3 avec dynamic colors, dark mode et theming.

1. Du XML View System à Compose

Jetpack Compose est le framework UI déclaratif officiel de Google depuis Compose 1.0 (juillet 2021). Il remplace progressivement le système XML View vieux de 15 ans. La philosophie change radicalement : au lieu de décrire une hiérarchie figée modifiée impérativement par findViewById + setters, on déclare une fonction qui prend un état et produit l'UI correspondante. Quand l'état change, Compose recompose uniquement les parties affectées.

Un @Composable est une fonction annotée pouvant émettre de l'UI. Elle peut appeler d'autres Composables (Composition), recevoir des paramètres (incluant des slots content: @Composable () -> Unit pour la composition flexible), et reçoit implicitement un Composer qui gère le slot table interne.

AspectXML View SystemJetpack Compose
ParadigmeImpératifDéclaratif
Langage UIXML + Kotlin/JavaKotlin pur
ÉtatMutation manuelle (setText, setVisibility)State + recomposition automatique
RéutilisationCustom Views complexesComposables triviaux
Preview IDELayout Editor@Preview avec multiples configs
AnimationsObjectAnimator, ViewPropertyAnimatoranimate*AsState, AnimatedVisibility
PerformanceInflate XML coûteuxRecomposition fine, skip si égal
"Jetpack Compose is Android's modern toolkit for building native UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs." — Android Developers — Jetpack Compose

2. State : remember, mutableStateOf, rememberSaveable

Compose étant un système réactif, tout doit être observable. L'API de base est mutableStateOf(value) qui retourne un MutableState<T> notifiant Compose de toute mutation via la propriété .value. Wrappé dans remember { }, cet état survit aux recompositions mais pas à la rotation. Avec rememberSaveable { }, il survit aussi aux changements de configuration et au process death.

Pattern state hoisting : un Composable enfant reçoit la valeur et un callback (value: String, onValueChange: (String) -> Unit) plutôt que de gérer son propre état. Cela rend le Composable stateless, testable et réutilisable. Le parent (souvent un ViewModel) détient la source de vérité.

3. Side effects : LaunchedEffect, DisposableEffect, derivedStateOf

Les Composables doivent rester purs : pas d'appels réseau, pas d'inscription à un listener, pas de timer. Pour exécuter du code en dehors de la composition, on utilise les Effect APIs.

LaunchedEffect(key1) lance une coroutine au début de la composition et l'annule si la clé change ou si le Composable quitte la composition. Usage typique : démarrer une animation, faire un appel suspend. DisposableEffect gère un cycle souscription/désinscription (BroadcastReceiver, LocationManager). derivedStateOf dérive un état d'un autre sans recomposition inutile. produceState convertit un state non-Compose (Flow, LiveData) en State Compose.

4. Navigation Compose

Navigation Compose remplace les Fragments + Jetpack Navigation XML pour les apps Compose-only. Le principe : un NavHost qui prend un NavController et un graph de destinations définies par composable("route/{arg}") { backStackEntry -> ... }.

Les arguments peuvent être typés (String, Int, Long, Boolean, Parcelable custom via NavType). La version 2.7+ supporte les type-safe routes via @Serializable Kotlin Serialization, éliminant les strings et les casts.

5. Material 3 et theming

Material 3 (Material You) est le design system Google depuis Android 12. Sur Android 12+, l'app peut adopter automatiquement la palette de couleurs extraite du fond d'écran utilisateur via dynamic colors : val colorScheme = if (Build.VERSION.SDK_INT >= 31) dynamicLightColorScheme(LocalContext.current) else lightColorScheme().

Le theming Compose s'articule autour de trois CompositionLocals : colorScheme, typography, shapes. On les expose via un MaterialTheme { content() } en racine. Le dark mode est géré via isSystemInDarkTheme().

✏️ Cas pratique : NavHost typé + screen Composable

@Composable
fun AppRoot() {
    MaterialTheme {
        val navController = rememberNavController()
        NavHost(navController, startDestination = "list") {
            composable("list") { ProductListScreen(onItemClick = { id ->
                navController.navigate("detail/$id")
            }) }
            composable(
                route = "detail/{id}",
                arguments = listOf(navArgument("id") { type = NavType.LongType })
            ) { backStackEntry ->
                val id = backStackEntry.arguments?.getLong("id") ?: return@composable
                ProductDetailScreen(id)
            }
        }
    }
}

@Composable
fun ProductListScreen(onItemClick: (Long) -> Unit, vm: ProductVm = hiltViewModel()) {
    val state by vm.state.collectAsStateWithLifecycle()
    when (val s = state) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> LazyColumn {
            items(s.products) { product ->
                ProductCard(product, onClick = { onItemClick(product.id) })
            }
        }
        is UiState.Error -> Text("Erreur : ${s.message}")
    }
}
💡 Hook performance : utilise @Stable et @Immutable sur tes data classes pour aider Compose à skipper les recompositions inutiles. Active le Compose Compiler Metrics dans le build.gradle pour traquer les Composables instables.
⚠️ Piège : ne jamais mettre mutableStateOf en dehors d'un remember. Sans remember, l'état est réinitialisé à chaque recomposition et tu observeras un bug silencieux (compteur qui reste à 0, formulaire qui se vide). Linter Compose le signale via UnrememberedMutableState.

Points-clés à retenir

  • Compose est déclaratif : on écrit ce que l'UI est, pas ce qu'elle devient.
  • State = remember { mutableStateOf(...) }, persistant via rememberSaveable.
  • Side effects : LaunchedEffect (coroutines), DisposableEffect (subscriptions), derivedStateOf (calculs dérivés).
  • Navigation Compose : NavHost + composable("route"), type-safe routes via Serialization.
  • Material 3 + dynamic colors Android 12+, dark mode via isSystemInDarkTheme().

Pour aller plus loin

Approfondissement technique

Maîtriser Jetpack Compose en 2026 ne se résume plus à connaître @Composable, remember et LazyColumn. Les équipes qui livrent des applications fluides sur des appareils d'entrée de gamme ont intégré dans leur quotidien la notion de stabilité, le suivi des recompositions inutiles, l'utilisation parcimonieuse de CompositionLocal, et l'arrivée du Strong Skipping Mode introduit avec Compose Compiler 1.5.4. Cette section approfondit les mécanismes internes du compilateur Compose, les optimisations de performance les plus rentables, et l'écosystème qui gravite autour : Accompanist, Compose Multiplatform et les libs tierces qui complètent les Material Components officiels.

Le compilateur Compose analyse chaque type passé en paramètre d'un composable pour déterminer s'il est stable. Un type est stable si son equals est cohérent (deux instances égales restent égales) et si toutes ses propriétés publiques sont elles-mêmes stables. Les data classes avec uniquement des val de types primitifs ou stables sont automatiquement stables. En revanche, une List<Item> issue de la stdlib n'est pas considérée stable (interface mutable potentielle), ce qui force la recomposition même quand le contenu n'a pas changé. La solution est soit d'utiliser kotlinx.collections.immutable.PersistentList, soit d'annoter manuellement la classe wrapper avec @Immutable ou @Stable.

Le Strong Skipping Mode change la donne : depuis Compose Compiler 1.5.4, activable via composeCompiler { enableStrongSkippingMode = true }, il permet au compilateur de skip un composable même si certains de ses paramètres sont instables, à condition que leur référence n'ait pas changé. Cette optimisation, devenue le défaut à partir de Compose 1.7, élimine en pratique la majorité des recompositions parasites sans nécessiter d'annotations manuelles. Mesurer son impact via les Compose Compiler Metrics reste un exercice essentiel : la flag kotlinc -P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination produit un rapport CSV listant les composables non-skippables et leur cause.

CompositionLocal, ProvidableCompositionLocal et anti-patterns

CompositionLocal est le mécanisme de Compose pour propager implicitement une valeur à travers l'arbre — l'équivalent du Context React. LocalContentColor, LocalDensity, LocalConfiguration sont les exemples canoniques. Créer son propre CompositionLocal via compositionLocalOf (qui invalide les lecteurs) ou staticCompositionLocalOf (qui invalide tout le sous-arbre, à réserver aux valeurs quasi-immuables comme un thème) est utile pour fournir un ViewModel partagé, une configuration utilisateur, ou un client réseau scopé.

L'anti-pattern à éviter est l'usage de CompositionLocal pour propager des données métier qui devraient transiter explicitement. Plus l'arbre devient profond, plus l'origine de la donnée devient opaque, et plus les tests deviennent fragiles. La règle est de réserver CompositionLocal aux préoccupations transverses (theming, accessibilité, localisation) et de passer les data via les paramètres ou via un ViewModel injecté.

Accompanist, Compose Multiplatform et libs tierces

Accompanist, maintenu par Google, a longtemps comblé les manques de Compose : Pager, Permissions, SystemUiController, Insets, FlowLayout. La plupart de ces modules sont aujourd'hui dépréciés au profit d'équivalents officiels intégrés à androidx.compose.foundation et androidx.compose.material3. Seuls quelques modules restent recommandés : accompanist-permissions pour la gestion runtime des permissions, accompanist-adaptive pour les layouts foldable/large screens.

Compose Multiplatform, porté par JetBrains, autorise désormais le déploiement d'UI Compose sur iOS (stable depuis 2024), desktop (Windows/macOS/Linux) et Web (Wasm en beta). L'architecture diffère légèrement d'Android car certaines API Material 3 ne sont disponibles que via le module commun, et les bridges natifs (caméra, permissions) restent à écrire par plateforme. Pour une équipe Android cherchant à étendre une app vers iOS sans embaucher d'iOSdev, Compose Multiplatform représente l'option la plus économique en 2026, à condition d'accepter une UI parfois moins idiomatique sur iOS qu'une app SwiftUI native.

Cas pratiques détaillés

Cas 1 — Optimisation d'une LazyColumn avec items stables

Une liste de 500 items qui scroll de manière saccadée est typiquement le symptôme d'items instables provoquant des recompositions en cascade. Le diagnostic passe par le Layout Inspector en mode « recomposition counts » ; la solution combine une key stable, des items @Immutable et un derivedStateOf pour le calcul d'état dérivé.

@Immutable
data class ProductUi(
    val id: Long,
    val name: String,
    val price: String,
    val imageUrl: String,
    val isFavorite: Boolean
)

@Composable
fun ProductList(
    products: ImmutableList<ProductUi>,
    onToggleFavorite: (Long) -> Unit
) {
    val listState = rememberLazyListState()
    val showFab by remember {
        derivedStateOf { listState.firstVisibleItemIndex > 10 }
    }

    Box {
        LazyColumn(state = listState) {
            items(
                items = products,
                key = { it.id },
                contentType = { "product" }
            ) { product ->
                ProductRow(
                    product = product,
                    onFavoriteClick = { onToggleFavorite(product.id) }
                )
            }
        }
        AnimatedVisibility(visible = showFab, modifier = Modifier.align(Alignment.BottomEnd)) {
            FloatingActionButton(onClick = { /* scroll top */ }) {
                Icon(Icons.Default.KeyboardArrowUp, contentDescription = null)
            }
        }
    }
}

Cas 2 — CompositionLocal pour un AnalyticsTracker

Propager un tracker analytics à tous les écrans sans passer un paramètre explicite à chaque composable est un cas légitime de CompositionLocal. La règle : fournir une valeur par défaut qui ne fait rien (no-op) pour faciliter les previews et les tests.

interface AnalyticsTracker {
    fun track(event: String, params: Map<String, Any> = emptyMap())
}

object NoOpTracker : AnalyticsTracker {
    override fun track(event: String, params: Map<String, Any>) = Unit
}

val LocalAnalytics = staticCompositionLocalOf<AnalyticsTracker> { NoOpTracker }

@Composable
fun App(tracker: AnalyticsTracker) {
    CompositionLocalProvider(LocalAnalytics provides tracker) {
        AppNavGraph()
    }
}

@Composable
fun CheckoutScreen() {
    val analytics = LocalAnalytics.current
    LaunchedEffect(Unit) {
        analytics.track("checkout_viewed")
    }
    // ... reste de l'écran
}

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
LazyColumn + key stableExcellenteExcellenteListes longues hétérogènes
Column + verticalScrollBonne (< 50 items)ExcellenteListes courtes statiques
LazyVerticalGridTrès bonneTrès bonneGalerie photos, catalogue
HorizontalPagerTrès bonneBonneOnboarding, swipe content
SubcomposeLayoutCoûteuseFaibleMesure dynamique enfants
derivedStateOfExcellenteBonneÉtat dérivé recalculé peu souvent
remember sans keyExcellenteExcellenteValeur stable au cycle composition
RecyclerView XMLBonneMoyenneLegacy / interop progressif

Pièges fréquents en production

  • Lambda non stable — Passer { vm.onClick(item.id) } à chaque recomposition crée une nouvelle instance de lambda, considérée instable. Sans Strong Skipping Mode, le composable enfant recompose inutilement. Solution : remember(item.id) { { vm.onClick(item.id) } } ou faire passer la lambda au ViewModel.
  • Modifier non remember — Construire un Modifier.padding(8.dp).background(...) dans le corps d'un composable réalloue à chaque recomposition. L'impact reste mineur grâce à la stabilité de Modifier, mais sur une liste de 500 items la GC pression devient visible. Solution : extraire les modifiers complexes en constantes ou les remember.
  • État non hoisté — Stocker un remember { mutableStateOf("") } dans un composable enfant empêche le parent de connaître l'état. Hoister l'état au parent (state hoisting) ou au ViewModel rend le composable stateless, testable et réutilisable.
  • SideEffect mal utilisé — Appeler une fonction non-Compose dans le corps d'un composable la déclenche à chaque recomposition. Wrapper dans LaunchedEffect(key1, key2) pour les coroutines, SideEffect { } pour les appels synchrones après chaque recomposition, DisposableEffect pour les ressources à libérer.
  • Animation interrompue — Lancer une animation dans un composable sans key approprié relance l'animation à chaque recomposition. Utiliser animateFloatAsState ou updateTransition qui gèrent la continuité automatiquement.
  • Theme global modifié à chaud — Modifier MaterialTheme.colorScheme via un CompositionLocal sans staticCompositionLocalOf provoque une recomposition de toute l'app à chaque changement de theme. Utiliser staticCompositionLocalOf pour les thèmes et accepter l'invalidation totale comme un coût acceptable au changement de mode jour/nuit.
  • State pas Saver-compatiblerememberSaveable ne sauvegarde que les types Bundle-compatibles. Pour une data class custom, fournir un Saver explicite ou utiliser @Parcelize et parcelableSaver.
  • Preview non isolée — Une preview qui dépend d'un ViewModel Hilt ou d'un Context complet ne compile pas hors d'une activity. Toujours créer des composables stateless paramétrés et des previews avec données fakes via @PreviewParameter.

Outils et écosystème

  • Compose Compiler Metrics — Génère un rapport sur les composables non-stables et les paramètres qui invalident le skipping. Activable via composeCompiler { reportsDestination.set(layout.buildDirectory.dir("compose_reports")) }. Voir developer.android.com/develop/ui/compose/performance/stability.
  • Layout Inspector — Intégré à Android Studio, affiche en temps réel l'arbre de composition, les recompositions par composable et les skips. Outil n°1 pour diagnostiquer un freeze de scroll.
  • Coil-Compose — Lib d'image asynchrone native Kotlin/Compose. AsyncImage(model = url) remplace Glide/Picasso avec une API plus propre et des transformations Compose-friendly.
  • Compose Destinations — Génère le code de navigation type-safe à partir d'annotations sur les composables d'écran. Élimine le boilerplate du NavHost manuel.
  • Voyager — Bibliothèque de navigation alternative pensée pour Compose Multiplatform, avec support nested navigation, tab navigation et bottom sheet navigation.
  • Molecule (CashApp) — Convertit un composable en StateFlow, permettant d'écrire la logique de presentation en Compose et de la consommer depuis n'importe quel framework UI.
  • Showkase — Génère un catalogue navigable de tous les composables et previews du projet, équivalent Storybook pour Compose.

Citations sources officielles

"Compose tracks state reads and writes for each composable function. When a state changes, Compose re-executes the composable functions that read that state — and only those functions. This is called intelligent recomposition." — developer.android.com/develop/ui/compose/mental-model
"Strong Skipping Mode allows Compose to skip recomposing a composable function even if its parameters are unstable, as long as their references haven't changed since the last composition." — developer.android.com Compose stability

Glossaire (10 termes)

TermeDéfinition
RecompositionRé-exécution d'un composable suite à un changement d'état lu
StabilityPropriété qu'un type a un equals cohérent et des champs stables
@ImmutableAnnotation garantissant qu'aucun champ ne change après construction
@StableAnnotation promettant des notifications de changement à Compose
State hoistingPattern remontant l'état vers le composable parent
CompositionLocalValeur propagée implicitement dans l'arbre de composition
ModifierDescription chainable de styles et comportements appliqués à un composable
derivedStateOfÉtat dérivé recalculé seulement quand ses dépendances changent
LaunchedEffectCoroutine liée au cycle de vie d'une composition
Strong SkippingMode compilateur skippant les composables à références inchangées

Ressources d'approfondissement

Continuez le parcours 🚀

Inscrivez-vous pour accéder aux 5 autres leçons + le quiz final.

Créer mon compte
🍪 Nous utilisons des cookies essentiels et, avec ton accord, des cookies analytiques. En savoir plus

⚙️ Préférences cookies

Choisis quels cookies tu acceptes — modifiable à tout moment.

🔐 Essentiels (obligatoires)Authentification, session, sécurité. Toujours actifs.
📊 Analytics anonymesMesure d'audience anonymisée — aucune donnée personnelle.
📣 MarketingPublicités ITAG pertinentes sur d'autres sites.
💬 Contactez-nous sur WhatsApp