Si vous venez du monde des XML et des RecyclerView, basculer sur Jetpack Compose ressemble à un rêve. On écrit moins de code, tout est réactif, $UI = f(State)$, c’est magique.

Sauf qu’en production, sur un projet qui grandit, la magie s’estompe vite si on ne fait pas gaffe. Un écran de liste un peu lourd, et paf : des micro-saccades au scroll, des batteries qui fondent, et des téléphones qui chauffent.

Après trois ans à casser et réparer des interfaces sous Compose, voici les 4 erreurs critiques que mon équipe et moi avons traquées, et les bonnes pratiques indispensables pour garder une appli à 60 (ou 120) FPS

1. Le piège de la List<T> standard : comment ruiner un scroll en 3 lignes

C’est l’erreur numéro un, celle que tout le monde fait au début. Vous avez un écran de flux (un feed), et votre état ressemble à ça :

Kotlin

data class FeedState(

val isLoading: Boolean = false,

val items: List<Post> = emptyList() // <- Le loup est caché ici

)

Sur le papier, rien à signaler. Sauf que pour le compilateur de Compose, l’interface List de Kotlin n’est pas considérée comme immuable. Pourquoi ? Parce que derrière une List, il peut se cacher une ArrayList mutable. Dans le doute, Compose refuse de considérer ce paramètre comme « stable ».

Le résultat ? À chaque fois que votre écran va se recomposer (par exemple, parce qu’un timer de pub se met à jour en haut de l’écran), Compose va se dire : « Tiens, la liste a PEUT-ÊTRE changé, je dois redessiner chaque élément de la liste. » Votre CPU passe sa vie à faire de la recomposition inutile.

La solution « terrain »

Pour forcer Compose à ignorer la recomposition si le contenu de la liste n’a pas bougé (ce qu’on appelle la skippability), vous avez deux options :

A. Option rapide (Annotation) : Taguer votre data class métier.

Kotlin

@Immutable // On certifie à Compose que rien ne bougera après création

data class Post(val id: String, val title: String)

B. Option propre (Collections immuables) : Utiliser la librairie officielle de collections immuables de Kotlin.

Kotlin

// Dans votre build.gradle.kts : implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:X.X.X")

data class FeedState(

val items: ImmutableList<Post> = persistentListOf()

)

2. Arrêtez de passer vos ViewModels en paramètres à vos sous-composables

Quand on est pressé, on a tendance à écrire ça :

Kotlin

@Composable

fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {

val state by viewModel.state.collectAsStateWithLifecycle()


// ...

UserProfileHeader(viewModel = viewModel) // <- Mauvaise idée

}


@Composable

fun UserProfileHeader(viewModel: MainViewModel) {

// On récupère la donnée directement depuis le VM

val user by viewModel.userFlow.collectAsStateWithLifecycle()

Text(text = user.name)

}

Pourquoi c’est une fausse bonne idée ? Vous venez de coupler votre joli composant d’UI UserProfileHeader à un ViewModel spécifique

  • Bonne chance pour afficher ce header dans l’outil @Preview d’Android Studio sans devoir mocker tout le ViewModel (et ses cas d’usage/repositories cachés derrière).
  • Bonne chance pour réutiliser ce header sur un autre écran avec un autre ViewModel.
La solution : Le State Hoisting (Élévation de l’état)

Séparez vos composants en deux catégories : les composants « Smart » (qui gèrent le ViewModel et le cycle de vie) et les composants « Dumb » (des fonctions pures qui ne reçoivent que de la donnée et renvoient des événements).

Kotlin

@Composable

fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {

val state by viewModel.state.collectAsStateWithLifecycle()


// Le composant Smart passe uniquement les données nécessaires

UserProfileHeader(

userName = state.userName,

onHeaderClick = { viewModel.onEvent(MainEvent.HeaderClicked) }

)

}


@Composable

fun UserProfileHeader(userName: String, onHeaderClick: () -> Unit) {

// Ce composant est 100% autonome, testable et preview-able !

Text(

text = userName,

modifier = Modifier.clickable { onHeaderClick() }

)

}

3. derivedStateOf : Votre joker contre les calculs lourds à répétition

Imaginez que vous devez afficher un bouton « Retour en haut » uniquement quand l’utilisateur a scrollé de plus de 5 éléments dans une LazyColumn. On écrit souvent ça :

Kotlin

val listState = rememberLazyListState()

val showScrollToTop = listState.firstVisibleItemIndex > 5 // <- Piège !

if (showScrollToTop) {

ScrollToTopButton()

}

Le problème ?listState.firstVisibleItemIndexchange à chaque pixel de scroll. Votre variable showScrollToTop va être réévaluée des dizaines de fois par seconde, déclenchant des recompositions en cascade pour rien, car la valeur reste false pendant la majeure partie du scroll.

La solution :

Quand un état dépend d’un autre état qui change très fréquemment, on utilise derivedStateOf. Cela permet de ne déclencher une recomposition que lorsque le résultat final du calcul change (quand on passe enfin de false à true).

Kotlin

val listState = rememberLazyListState()

// Le bloc ne notifiera l'UI que si le booléen change d'état

val showScrollToTop by remember {

derivedStateOf { listState.firstVisibleItemIndex > 5 }

}

4. collectAsState() vs collectAsStateWithLifecycle(): Ne siphonnez plus la batterie de vos utilisateurs

Quand vous exposez un StateFlow depuis votre ViewModel, vous devez le consommer dans Compose. La tentation est grande d’utiliser le raccourci historique : .collectAsState().

Le drame caché : Si l’utilisateur reçoit un appel ou passe sur une autre application (votre appli passe en arrière-plan), .collectAsState() continue d’écouter activement les mises à jour du flux en arrière-plan. Si votre ViewModel est branché sur un flux de géolocalisation ou des websockets, l’appli va continuer à consommer des ressources pour mettre à jour une UI… que l’utilisateur ne voit même pas.

La solution « Pro » :

Ajoutez l’artefact de cycle de vie de Compose (androidx.lifecycle:lifecycle-runtime-compose) et utilisez systématiquement :

Kotlin

// Arrête automatiquement la collecte quand l'écran n'est plus visible (onStop)

val state by viewModel.state.collectAsStateWithLifecycle()
Le mot de la fin

Développer avec Jetpack Compose, c’est accepter de lâcher le contrôle direct sur les pixels pour faire confiance au framework. Mais pour que cette confiance fonctionne, il faut lui donner les bons indices.

Si vous appliquez déjà ces 4 règles (utiliser des collections immuables, pratiquer le State Hoisting, isoler les états dynamiques avec derivedStateOf et sécuriser vos collectes avec collectAsStateWithLifecycle), vous éliminerez 90% des bugs de performance classiques rencontrés en production

Besoin d’aller plus loin sur vos projets mobiles ?

Ces 4 erreurs ne sont que la partie émergée de l’iceberg quand on développe une application mobile performante et durable. Chez Atecna, notre équipe accompagne les entreprises sur l’ensemble du cycle de vie de leurs projets mobiles : architecture, performance, accessibilité et maintenance.