Composables, state, side effects, Navigation, Material 3 : construire des interfaces réactives en Kotlin pur.
remember, mutableStateOf, rememberSaveable.LaunchedEffect, DisposableEffect, derivedStateOf, produceState).NavHost.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.
| Aspect | XML View System | Jetpack Compose |
|---|---|---|
| Paradigme | Impératif | Déclaratif |
| Langage UI | XML + Kotlin/Java | Kotlin pur |
| État | Mutation manuelle (setText, setVisibility) | State + recomposition automatique |
| Réutilisation | Custom Views complexes | Composables triviaux |
| Preview IDE | Layout Editor | @Preview avec multiples configs |
| Animations | ObjectAnimator, ViewPropertyAnimator | animate*AsState, AnimatedVisibility |
| Performance | Inflate XML coûteux | Recomposition 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
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é.
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.
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.
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().
@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}")
}
}@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.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.remember { mutableStateOf(...) }, persistant via rememberSaveable.LaunchedEffect (coroutines), DisposableEffect (subscriptions), derivedStateOf (calculs dérivés).NavHost + composable("route"), type-safe routes via Serialization.isSystemInDarkTheme().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 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, 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.
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)
}
}
}
}
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
}
| Approche | Performance | Lisibilité | Cas d'usage |
|---|---|---|---|
| LazyColumn + key stable | Excellente | Excellente | Listes longues hétérogènes |
| Column + verticalScroll | Bonne (< 50 items) | Excellente | Listes courtes statiques |
| LazyVerticalGrid | Très bonne | Très bonne | Galerie photos, catalogue |
| HorizontalPager | Très bonne | Bonne | Onboarding, swipe content |
| SubcomposeLayout | Coûteuse | Faible | Mesure dynamique enfants |
| derivedStateOf | Excellente | Bonne | État dérivé recalculé peu souvent |
| remember sans key | Excellente | Excellente | Valeur stable au cycle composition |
| RecyclerView XML | Bonne | Moyenne | Legacy / interop progressif |
{ 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.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.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.LaunchedEffect(key1, key2) pour les coroutines, SideEffect { } pour les appels synchrones après chaque recomposition, DisposableEffect pour les ressources à libérer.key approprié relance l'animation à chaque recomposition. Utiliser animateFloatAsState ou updateTransition qui gèrent la continuité automatiquement.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.rememberSaveable ne sauvegarde que les types Bundle-compatibles. Pour une data class custom, fournir un Saver explicite ou utiliser @Parcelize et parcelableSaver.@PreviewParameter.composeCompiler { reportsDestination.set(layout.buildDirectory.dir("compose_reports")) }. Voir developer.android.com/develop/ui/compose/performance/stability.AsyncImage(model = url) remplace Glide/Picasso avec une API plus propre et des transformations Compose-friendly.NavHost manuel."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
| Terme | Définition |
|---|---|
| Recomposition | Ré-exécution d'un composable suite à un changement d'état lu |
| Stability | Propriété qu'un type a un equals cohérent et des champs stables |
| @Immutable | Annotation garantissant qu'aucun champ ne change après construction |
| @Stable | Annotation promettant des notifications de changement à Compose |
| State hoisting | Pattern remontant l'état vers le composable parent |
| CompositionLocal | Valeur propagée implicitement dans l'arbre de composition |
| Modifier | Description chainable de styles et comportements appliqués à un composable |
| derivedStateOf | État dérivé recalculé seulement quand ses dépendances changent |
| LaunchedEffect | Coroutine liée au cycle de vie d'une composition |
| Strong Skipping | Mode compilateur skippant les composables à références inchangées |
Inscrivez-vous pour accéder aux 5 autres leçons + le quiz final.
Créer mon compteChoisis quels cookies tu acceptes — modifiable à tout moment.