Résolution pratique de problèmes de performances dans Jetpack Compose

1. Avant de commencer

Dans cet atelier de programmation, vous allez apprendre à améliorer les performances d'exécution d'une application Compose. Vous suivrez une approche scientifique pour évaluer, déboguer et optimiser les performances. Vous vous intéresserez à plusieurs problèmes de performances détectés grâce au traçage système et modifierez le code d'exécution non performant dans une application exemple, qui contient plusieurs écrans représentant différentes tâches. Les écrans sont construits de manière différente et se présentent de la manière suivante :

  • Le premier écran est une liste à deux colonnes avec des éléments d'image et des balises en haut des éléments. Ici, vous optimisez des composables lourds.

8afabbbbbfc1d506.gif

  • Les deuxième et troisième écrans contiennent un état qui se recompose fréquemment. Ici, vous supprimez les recompositions inutiles afin d'optimiser les performances.

f0ccf14d1c240032.gif 51dc23231ebd5f1a.gif

  • Le dernier écran contient des éléments instables. Ici, vous stabilisez les éléments à l'aide de différentes techniques.

127f2e4a2fc1a381.gif

Conditions préalables

  • Vous devez savoir créer des applications Compose.
  • Vous disposez des connaissances de base en matière de tests ou d'exécution de macrobenchmarks.

Objectifs de l'atelier

  • Savoir identifier les problèmes de performances grâce aux traces système et au traçage de composition.
  • Développer des applications Compose performantes qui s'affichent sans problème.

Ce dont vous avez besoin

2. Configuration

Pour commencer, procédez comme suit :

  1. Clonez le dépôt GitHub :
$ git clone https://github.com/android/codelab-android-compose.git

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

  1. Ouvrez le projet PerformanceCodelab qui contient les branches suivantes :
  • main : contient le code de démarrage de ce projet, que vous allez modifier tout au long de l'atelier.
  • end : contient le code de solution pour cet atelier.

Nous vous recommandons de commencer avec la branche main et suivre l'atelier de programmation étape par étape, à votre rythme.

  1. Si vous souhaitez voir le code de solution, exécutez la commande suivante :
$ git clone -b end https://github.com/android/codelab-android-compose.git

Vous pouvez également télécharger le code de solution :

Facultatif : traces système utilisées dans cet atelier de programmation

Vous exécuterez plusieurs benchmarks qui captureront les traces système au cours de l'atelier.

Si vous n'arrivez pas à exécuter ces tests, voici une liste de traces système que vous pouvez télécharger à la place :

3. Approche pour résoudre les problèmes de performances

Il est possible de repérer une interface utilisateur lente et non performante en explorant simplement l'application. Mais avant de vous jeter à l'eau et de commencer à corriger le code sur la base de vos hypothèses, vous devez évaluer les performances de votre code pour comprendre si vos modifications l'amélioreront.

Pendant le développement, avec un build debuggable de votre application, vous pourriez remarquer que quelque chose n'est pas aussi performant que nécessaire et vous pourriez être tenté de résoudre ce problème. Cependant, les performances d'une application debuggable ne sont pas représentatives de ce que verront vos utilisateurs, il est donc important de confirmer ce problème de performances à l'aide d'une application non-debuggable. Dans une application debuggable, tout le code doit être interprété par l'environnement d'exécution.

En ce qui concerne les performances dans Compose, il n'existe pas de règle absolue à suivre pour implémenter une fonctionnalité particulière. Vous devez éviter de procéder à certaines actions trop rapidement :

  • Ne cherchez pas à corriger chaque paramètre instable qui se glisse dans votre code.
  • Ne supprimez pas les animations qui entraînent une recomposition d'un composable.
  • Ne procédez pas à des optimisations difficiles à lire sur la base de votre intuition.

Toutes ces modifications doivent être effectuées en connaissance de cause, à l'aide des outils disponibles, afin de s'assurer qu'elles permettent de résoudre le problème de performances.

Lorsque vous traitez des problèmes de performances, vous devez suivre cette approche scientifique :

  1. Établissez les performances initiales en les mesurant.
  2. Déterminez la cause du problème.
  3. Modifiez le code d'après vos observations.
  4. Évaluez les performances et comparez-les aux performances initiales.
  5. Recommencez.

Si vous ne suivez aucune méthode structurée, certains changements pourraient améliorer les performances, mais d'autres les dégrader, et vous pourriez revenir au point de départ.

Nous vous recommandons de regarder la vidéo suivante sur l'amélioration des performances des applications avec Compose, qui explique comment résoudre les problèmes de performances et donne même quelques conseils pour les améliorer.

Générer des profils de référence

Avant de vous plonger dans la recherche de problèmes de performances, générez un profil de référence pour votre application. Sur Android 6 (niveau d'API 23) et versions ultérieures, les applications exécutent du code interprété au moment de l'exécution et compilé just-in-time (JIT, ou compilation à la volée) et ahead-of-time (AOT, ou compilation anticipée) au moment de l'installation. Le code interprété et faisant l'objet d'une compilation JIT s'exécute plus lentement qu'avec une compilation AOT, mais prends moins de place sur le disque et dans la mémoire, c'est pourquoi tous les codes ne doivent pas faire l'objet d'une compilation AOT

En implémentant des profils de référence, vous pouvez améliorer la vitesse de démarrage de votre application de 30 % et diviser par huit le code s'exécutant en mode JIT lors de l'exécution, comme le montre l'image suivante, basée sur l'application exemple Now in Android :

b51455a2ca65ea8.png

Pour plus d'informations sur les profils de référence, consultez les ressources suivantes :

Évaluer les performances

Pour évaluer les performances, nous vous conseillons d'implémenter et d'écrire des benchmarks avec Jetpack Macrobenchmark. Les macrobenchmarks sont des tests d'instrumentation qui interagissent avec votre application comme le ferait un utilisateur tout en surveillant les performances de l'application. Cela signifie qu'ils ne polluent pas le code de l'application avec du code de test et fournissent donc des informations fiables sur les performances.

Dans cet atelier de programmation, nous avons déjà mis en place le codebase et écrit les benchmarks pour nous concentrer directement sur la résolution des problèmes de performances. Si vous ne savez pas comment configurer et utiliser Macrobenchmark dans votre projet, consultez les ressources suivantes :

Avec Macrobenchmark, vous pouvez choisir parmi ces modes de compilation :

  • None : réinitialise l'état de la compilation et exécute tout en mode JIT.
  • Partial : précompile l'application avec des profils de référence et/ou des itérations d'échauffement, et l'exécute en mode JIT.
  • Full : précompile l'ensemble du code de l'application, de sorte qu'aucun code ne s'exécute en mode JIT.

Dans cet atelier de programmation, vous n'utiliserez que le mode CompilationMode.Full() pour les benchmarks, car vous ne vous intéressez qu'aux modifications que vous apportez au code, et non à l'état de compilation de l'application. Cette approche vous permet de réduire la variance causée par l'exécution du code en mode JIT, qui doit être réduite lors de l'implémentation de profils de référence personnalisés. Attention, le mode Full peut affecter le démarrage de l'application. Ne l'utilisez donc pas pour les tests évaluant le démarrage de l'application, mais uniquement pour les tests mesurant les améliorations des performances d'exécution.

Lorsque vous avez fini d'améliorer les performances et que vous souhaitez les vérifier au moment de l'installation de l'application par les utilisateurs, utilisez le mode CompilationMode.Partial() qui utilise les profils de référence.

Dans la section suivante, vous apprendrez à lire les traces pour trouver les problèmes de performances.

4. Analyser les performances avec le traçage système

Avec un build debuggable de votre application, vous pouvez utiliser l'outil d'inspection de la mise en page avec le compteur de compositions pour comprendre rapidement si quelque chose se recompose trop souvent.

b7edfea340674732.gif

Cependant, il ne s'agit que d'une partie de l'étude globale des performances, car vous n'obtenez que des mesures indirectes et non le temps réel de rendu de ces éléments composables. Le fait qu'un élément se recompose N fois n'a pas beaucoup d'importance si la durée totale est inférieure à une milliseconde. Il en est autrement pour un élément qui se compose seulement une ou deux fois, mais pour une durée totale de 100 millisecondes. Il arrive souvent qu'un composable ne compose qu'une seule fois, mais que cela prenne trop de temps et ralentisse votre écran.

Pour étudier de manière fiable les problèmes de performances et vous donner une idée de ce que fait votre application et si elle prend plus de temps que prévu, vous pouvez utiliser le traçage système avec le traçage de composition.

Le traçage système vous donne des informations temporelles sur tout ce qui se passe dans votre application. Il n'ajoute aucune surcharge à votre application et vous pouvez donc le conserver dans l'application de production sans avoir à vous soucier des effets négatifs sur les performances.

Configurer le traçage de composition

Compose fournit automatiquement des informations sur ses phases d'exécution, par exemple lorsque quelque chose se recompose ou lorsqu'une mise en page différée précharge des éléments. Toutefois, ces informations ne sont pas suffisantes pour déterminer quelle section pourrait poser problème. Vous pouvez obtenir plus d'informations en configurant le traçage de composition, qui vous donne le nom de chaque composable qui a été composé pendant la trace. Cela vous permet de commencer à étudier les problèmes de performances sans avoir à ajouter de nombreuses sections trace("label") personnalisées.

Pour activer le traçage de composition, procédez comme suit :

  1. Ajoutez la dépendance runtime-tracing au module :app.
implementation("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")

À ce stade, vous pourriez enregistrer une trace système avec le profileur Android Studio qui inclurait toutes les informations, mais nous utiliserons le Macrobenchmark pour les mesures de performances et l'enregistrement des traces système.

  1. Ajoutez des dépendances supplémentaires au module :measure pour permettre le traçage de composition avec Macrobenchmark :
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
  1. Ajoutez l'argument d'instrumentation androidx.benchmark.fullTracing.enable=true au fichier build.gradle du module :measure :
defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.fullTracing.enable"] = "true"
}

Pour plus d'informations sur la manière de configurer le traçage de composition, et notamment sur la manière de l'utiliser à partir d'un terminal, consultez la documentation.

Capturer les performances initiales avec Macrobenchmark

Il existe plusieurs façons de récupérer un fichier de trace système. Par exemple, vous pouvez l'enregistrer avec le profileur Android Studio, le capturer sur un appareil, ou récupérer une trace système enregistrée avec Macrobenchmark. Dans cet atelier de programmation, vous utilisez les traces capturées par la bibliothèque Macrobenchmark.

Ce projet contient des benchmarks dans le module :measure que vous pouvez exécuter pour obtenir les mesures de performances. Les benchmarks du projet sont configurés pour n'exécuter qu'une seule itération afin de gagner du temps dans cet atelier de programmation. Dans l'application réelle, il est recommandé d'avoir au moins dix itérations si la variance de sortie est élevée.

Pour capturer les performances initiales, utilisez le test AccelerateHeavyScreenBenchmark qui fait défiler l'écran de la première fenêtre de tâches en suivant ces étapes :

  1. Ouvrez le fichier AccelerateHeavyScreenBenchmark.kt.
  2. Exécutez le benchmark depuis la gouttière située en regard de la classe de benchmark :

e93fb1dc8a9edf4b.png

Ce benchmark fait défiler l'écran Task 1 et capture le temps de rendu et

les sections de trace personnalisées.

8afabbbbbfc1d506.gif

Une fois le benchmark terminé, vous devriez voir les résultats dans le volet de sortie d'Android Studio :

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0

Les mesures importantes dans la sortie sont les suivantes :

  • frameDurationCpuMs : indique le temps nécessaire au rendu des frames. Plus ce temps est court, mieux c'est.
  • frameOverrunMs : indique le temps passé au-delà de la limite de trame, y compris le travail sur le GPU. Un nombre négatif est une bonne chose, car cela signifie qu'il y avait encore du temps.

Les autres mesures, telles que la mesure ImagePlaceholderMs, utilisent des sections de trace personnalisées et indiquent la durée totale de toutes ces sections dans le fichier de trace et leur nombre d'occurrences à l'aide de la mesure ImagePlaceholderCount.

Toutes ces mesures peuvent nous aider à comprendre si les changements que nous apportons à notre codebase améliorent les performances.

Lire le fichier de trace

Vous pouvez lire la trace système à partir d'Android Studio ou de l'outil Web Perfetto.

Alors que le profileur Android Studio est un bon moyen d'ouvrir rapidement une trace et d'afficher le processus de votre application, Perfetto offre des capacités d'investigation plus approfondies pour tous les processus en cours d'exécution sur un système avec des requêtes SQL puissantes et plus encore. Dans cet atelier de programmation, vous utilisez Perfetto pour analyser les traces système.

  1. Accédez au site Web Perfetto qui charge le tableau de bord de l'outil.
  2. Localisez les traces système capturées par Macrobenchmark sur votre système de fichiers hôte, qui sont sauvegardées dans le dossier [module]/outputs/connected_android_test_additional_output/benchmarkRelease/connected/[device]/. Chaque itération du benchmark enregistre un fichier de trace distinct, chacun contenant les mêmes interactions avec votre application.

51589f24d9da28be.png

  1. Faites glisser le fichier AccelerateHeavyScreenBenchmark_...iter000...perfetto-trace vers l'interface utilisateur de Perfetto et attendez que le fichier de trace soit chargé.
  2. Facultatif : si vous n'êtes pas en mesure d'exécuter le benchmark et de générer le fichier de trace, téléchargez notre fichier de trace et faites-le glisser vers Perfetto :

547507cdf63ae73.gif

  1. Trouvez le processus de votre application, nommé com.compose.performance. En général, l'application au premier plan se trouve sous les informations sur le matériel et sous quelques informations système.
  2. Ouvrez le menu déroulant avec le nom du processus de l'application. La liste des threads en cours d'exécution dans votre application apparaît. Gardez le fichier de trace ouvert, car vous en aurez besoin à l'étape suivante.

582b71388fa7e8b.gif

Pour trouver un problème de performances dans votre application, vous pouvez utiliser les chronologies attendues et réelles en haut de la liste de threads de votre application :

1bd6170d6642427e.png

L'Expected Timeline (chronologie attendue) vous indique quand le système s'attend à ce que les frames produits par votre application affichent une interface utilisateur fluide et performante, soit, dans ce cas, 16 ms et 600 µs (1000 ms/60). La Actual Timeline (chronologie réelle) indique la durée réelle des frames produits par votre application, y compris les tâches effectuées par le GPU.

Différentes couleurs peuvent s'afficher :

  • Frame vert : le frame a été généré dans les délais.
  • Frame rouge : le frame saccadé a pris plus de temps que prévu. Vous devez examiner le travail effectué dans ces frames afin d'éviter des problèmes de performances.
  • Frame vert clair : le frame a été généré dans le délai imparti, mais affiché tardivement, ce qui a entraîné une augmentation de la latence d'entrée.
  • Frame jaune : le frame était saccadé, mais l'application n'en était pas la cause.

Lorsque l'interface utilisateur est affichée à l'écran, les modifications doivent prendre moins de temps que la durée prévue par votre appareil pour créer un frame. Historiquement, cette durée était d'environ 16,6 ms étant donné que la fréquence d'actualisation de l'écran était de 60 Hz, mais pour les appareils Android modernes, elle peut être d'environ 11 ms, voire moins, car la fréquence d'actualisation de l'écran est de 90 Hz ou plus. Cette durée peut différer pour chaque frame en raison de fréquences d'actualisation variables.

Par exemple, si votre interface utilisateur est composée de 16 éléments, chaque élément a environ 1 ms pour être créé afin d'éviter tout saut de frame. En revanche, si vous n'avez qu'un seul élément, tel qu'un lecteur vidéo, cela peut prendre jusqu'à 16 ms pour le composer sans à-coup.

Comprendre le graphique d'appel du traçage système

L'image suivante est un exemple de version simplifiée d'une trace système montrant une recomposition.

8f16db803ca19a7d.png

Chaque barre, du haut vers le bas, correspond à la durée totale des barres situées en dessous. Les barres correspondent également aux sections de code des fonctions appelées. Les appels Compose recomposent d'après votre hiérarchie de composition. MaterialTheme est le premier composable. Dans MaterialTheme se trouve une composition locale fournissant les informations de thématisation. Le composable HomeScreen est ensuite appelé. Le composable de l'écran d'accueil appelle les composables MyImage et MyButton dans le cadre de sa composition.

Les écarts dans les traces système proviennent de l'exécution de code non tracé, car les traces système ne montrent que le code qui est marqué pour le traçage. L'exécution du code se produit après l'appel de MyImage, mais avant celui de MyButton et prend le temps nécessaire pour combler l'écart.

Dans l'étape suivante, vous allez analyser la trace que vous avez capturée à l'étape précédente.

5. Accélérer des composables lourds

Lorsque vous essayez d'optimiser les performances de votre application, vous devez commencer par rechercher les éléments composables lourds ou une tâche de longue durée sur le thread principal. La définition d'une tâche de longue durée peut varier en fonction de la complexité de votre interface utilisateur et du temps alloué pour la composer.

Ainsi, si un frame est perdu, vous devez trouver les éléments composables qui prennent trop de temps et les accélérer en déchargeant le thread principal ou en ignorant une partie des tâches qu'ils effectuent sur celui-ci.

Pour analyser la trace du test AccelerateHeavyScreenBenchmark, procédez comme suit :

  1. Ouvrez la trace système que vous avez enregistrée à l'étape précédente.
  2. Faites un zoom avant sur le premier grand frame, qui contient l'initialisation de l'interface utilisateur après le chargement des données. Son contenu ressemble au frame suivant :

838787b87b14bbaf.png

Dans la trace, vous pouvez voir qu'il se passe beaucoup de choses à l'intérieur d'un frame, situé sous la section Choreographer#doFrame. Vous pouvez voir sur l'image que la plus grande partie de la tâche provient du composable qui contient la section ImagePlaceholder, qui charge une image volumineuse.

Ne pas charger d'images volumineuses dans le thread principal

Il peut paraître évident de charger des images asynchrones à partir d'un réseau en utilisant la bibliothèque Coil ou Glide, mais qu'en est-il si vous devez afficher une image locale volumineuse dans votre application ?

La fonction composable painterResource qui charge une image à partir de ressources charge l'image sur le thread principal pendant la composition. Ainsi, si votre image est volumineuse, elle peut bloquer le thread principal en demandant l'exécution de certaines tâches.

Dans votre cas, vous pouvez remarquer que le problème se situe au niveau de l'espace réservé pour l'image asynchrone. Le composable painterResource charge un espace réservé pour l'image qui met environ 23 ms à se charger.

c83d22c3870655a7.jpeg

Il existe plusieurs moyens de résoudre ce problème :

  • En chargeant l'image de manière asynchrone.
  • En réduisant la taille de l'image pour qu'elle se charge plus rapidement.
  • En utilisant un drawable vectoriel qui s'adapte à la taille requise.

Pour résoudre ce problème de performances, suivez les étapes suivantes :

  1. Accédez au fichier AccelerateHeavyScreen.kt.
  2. Localisez le composable imagePlaceholder() qui charge l'image. L'espace réservé pour l'image a des dimensions de 1600x1600 px, ce qui est nettement trop grand pour ce qu'elle montre.

53b34f358f2ff74.jpeg

  1. Modifiez le drawable en R.drawable.placeholder_vector :
@Composable
fun imagePlaceholder() =
    trace("ImagePlaceholder") { painterResource(R.drawable.placeholder_vector) }
  1. Réexécutez le test AccelerateHeavyScreenBenchmark, ce qui recompile l'application et évalue à nouveau la trace système.
  2. Faites glisser la trace système vers le tableau de bord Perfetto.

Vous pouvez également télécharger la trace :

  1. Cherchez la section de trace ImagePlaceholder, qui vous montre directement la partie améliorée.

abac4ae93d599864.png

  1. Vous pouvez constater que la fonction ImagePlaceholder ne bloque plus autant le thread principal.

8e76941fca0ae63c.jpeg

En tant que solution alternative dans l'application réelle, ce n'est peut-être pas un espace réservé pour l'image qui pose problème, mais un élément visuel. Dans ce cas, vous pouvez utiliser le composable rememberAsyncImage de la bibliothèque Coil, qui charge le composable de manière asynchrone. Cette solution affichera un espace vide jusqu'à ce que l'espace réservé soit chargé, sachez que vous pourriez donc avoir besoin d'un espace réservé pour ces types d'images.

D'autres dysfonctionnements persistent, et vous les aborderez à l'étape suivante.

6. Décharger une opération lourde sur un thread d'arrière-plan

Si vous continuez d'examiner le même élément à la recherche de problèmes supplémentaires, vous rencontrerez des sections intitulées binder transaction, qui durent environ 1 ms chacune.

5c08376b3824f33a.png

Les sections binder transaction indiquent qu'une communication inter-processus a eu lieu entre votre processus et un processus du système. Il s'agit d'un moyen normal de récupérer certaines informations du système, par exemple un service système.

Ces transactions sont incluses dans de nombreuses API communiquant avec le système. Par exemple, lors de la récupération d'un service système avec getSystemService, de l'enregistrement d'un broadcast receiver ou de la demande d'un ConnectivityManager.

Malheureusement, ces transactions ne fournissent pas beaucoup d'informations sur ce qu'elles demandent, vous devez donc analyser votre code sur les utilisations d'API mentionnées et ajouter une section trace personnalisée pour vous assurer qu'il s'agit bien de la partie problématique.

Pour améliorer les transactions de binder, procédez comme suit :

  1. Ouvrez le fichier AccelerateHeavyScreen.kt.
  2. Localisez le composable PublishedText. Ce composable formate une date avec le fuseau horaire actuel et enregistre un objet BroadcastReceiver qui garde la trace des changements de fuseau horaire. Il contient une variable d'état currentTimeZone dont la valeur initiale est le fuseau horaire par défaut du système, ainsi qu'un DisposableEffect qui enregistre un broadcast receiver pour les changements de fuseau horaire. Enfin, ce composable affiche une date formatée avec Text. DisposableEffect, qui est un bon choix dans ce scénario parce que vous avez besoin d'un moyen de désenregistrer le broadcast receiver, ce qui est fait dans le lambda onDispose. La partie problématique, cependant, est que le code à l'intérieur de DisposableEffect bloque le thread principal :
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        // TODO Codelab task: Wrap with a custom trace section
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))

        onDispose { context.unregisterReceiver(receiver) }
    }

    Text(
        text = published.format(currentTimeZone),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Encapsulez le context.registerReceiver dans un appel trace pour vous assurer qu'il est bien la cause des binder transactions :
trace("PublishDate.registerReceiver") {
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
}

En général, un code s'exécutant aussi longtemps sur le thread principal ne devrait pas poser beaucoup de problèmes, mais le fait que cette transaction s'exécute pour chaque élément visible à l'écran pourrait en poser. En supposant qu'il y ait six éléments visibles à l'écran, ils doivent être composés avec le premier frame. Ces appels peuvent à eux seuls prendre 12 ms, ce qui correspond presque à la durée totale d'un frame.

Pour résoudre ce problème, vous devez décharger l'enregistrement du broadcast sur un autre thread. Vous pouvez le faire à l'aide des coroutines.

  1. Obtenez un champ d'application lié au cycle de vie du composable val scope = rememberCoroutineScope().
  2. À l'intérieur de l'effet, lancez une coroutine sur un distributeur autre que Dispatchers.Main. Par exemple, Dispatchers.IO ici. Ainsi, l'enregistrement du broadcast ne bloque pas le thread principal, mais l'état actuel currentTimeZone est conservé dans le thread principal.
val scope = rememberCoroutineScope()

DisposableEffect(Unit) {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            currentTimeZone = TimeZone.currentSystemDefault()
        }
    }

    // launch the coroutine on Dispatchers.IO
    scope.launch(Dispatchers.IO) {
        trace("PublishDate.registerReceiver") {
            context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
        }
    }

    onDispose { context.unregisterReceiver(receiver) }
}

Il reste encore une étape pour en finir avec cette optimisation. Vous n'avez pas besoin d'un broadcast receiver pour chaque élément de la liste, mais que pour un. Vous devez le hisser !

Vous pouvez soit le hisser et transmettre le paramètre de fuseau horaire dans l'arbre des composables, soit, étant donné qu'il n'est pas utilisé à de nombreux endroits dans votre interface utilisateur, utiliser une composition locale.

Pour les besoins de cet atelier de programmation, vous gardez le broadcast receiver dans l'arbre des composables. Cependant, dans l'application réelle, il peut être intéressant de le séparer dans une couche de données pour éviter de polluer le code de l'interface utilisateur.

  1. Définissez la composition locale avec le fuseau horaire par défaut du système :
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
  1. Mettez à jour le composable ProvideCurrentTimeZone qui prend un lambda content pour transmettre le fuseau horaire actuel :
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    var currentTimeZone = TODO()

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Sortez le DisposableEffect du composable PublishedText dans le nouveau pour l'y hisser, et remplacez le currentTimeZone par l'état et l'effet secondaire :
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        scope.launch(Dispatchers.IO) {
            trace("PublishDate.registerReceiver") {
                context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
            }
        }

        onDispose { context.unregisterReceiver(receiver) }
    }

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Encapsulez un composable dans lequel vous voulez que la composition locale soit valide avec ProvideCurrentTimeZone. Vous pouvez encapsuler l'ensemble du AccelerateHeavyScreen, comme le montre l'extrait de code suivant :
@Composable
fun AccelerateHeavyScreen(items: List<HeavyItem>, modifier: Modifier = Modifier) {
    // TODO: Codelab task: Wrap this with timezone provider
    ProvideCurrentTimeZone {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp)
        ) {
            ScreenContent(items = items)

            if (items.isEmpty()) {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}
  1. Modifiez le composable PublishedText pour qu'il ne contienne que la fonctionnalité de formatage de base et regardez la valeur actuelle de la composition locale via LocalTimeZone.current :
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    Text(
        text = published.format(LocalTimeZone.current),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Relancez le benchmark, qui compile l'application.

Vous pouvez également télécharger la trace système avec le code corrigé :

  1. Faites glisser le fichier de trace vers le tableau de bord Perfetto. Toutes les sections binder transactions ont disparu du fil principal.
  2. Recherchez le nom de la section qui est similaire à celui de l'étape précédente. Vous pouvez le trouver dans l'un des autres threads créés par les coroutines (DefaultDispatch) :

87feee260f900a76.png

7. Supprimer les sous-compositions inutiles

Vous avez déplacé le code lourd du thread principal, de sorte qu'il ne bloque plus la composition. Des améliorations sont encore possibles. Vous pouvez supprimer certaines surcharges inutiles sous la forme d'un composable LazyRow dans chaque élément.

Dans l'exemple, chaque élément contient une rangée de balises, comme le montre l'image suivante :

e821c86604d3e670.png

Cette rangée est implémentée avec un composable LazyRow parce que l'écriture est facile. Transmettez les éléments au composable LazyRow, qui s'occupera du reste :

@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    // TODO: remove unnecessary lazy layout
    LazyRow(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        items(tags) { ItemTag(it) }
    }
}

Le problème est que, bien que les mises en page Lazy soient excellentes dans les cas où vous avez beaucoup plus d'éléments que la taille limitée, elles entraînent un coût supplémentaire, qui n'est pas nécessaire lorsque la composition saccadée n'est pas requise.

Étant donné la nature des composables Lazy, qui utilisent un composable SubcomposeLayout composable, ils sont toujours présentés en plusieurs parties, d'abord le conteneur, puis les éléments actuellement visibles à l'écran. Vous pouvez également trouver une trace compose:lazylist:prefetch dans la trace système, qui indique que des éléments supplémentaires entrent dans la fenêtre d'affichage et qu'ils sont donc préchargés pour être prêts à l'avance.

b3dc3662b5885a2e.jpeg

Pour déterminer approximativement combien de temps cela prend dans votre cas, ouvrez le même fichier de trace. Vous pouvez voir qu'il y a des sections détachées de l'élément parent. Chaque élément se compose de l'élément à composer suivi des éléments de balise. De cette manière, chaque élément représente environ 2,5 millisecondes de temps de composition, ce qui, si l'on multiplie par le nombre d'éléments visibles, demande un travail considérable.

a204721c80497e0f.jpeg

Pour régler ce problème, procédez comme suit :

  1. Accédez au fichier AccelerateHeavyScreen.kt et localisez le composable ItemTags.
  2. Modifiez l'implémentation LazyRow en un composable Row qui effectue une itération vers la liste tags dans l'extrait suivant :
@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth()
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        tags.forEach { ItemTag(it) }
    }
}
  1. Relancez le benchmark, qui compile également l'application.
  2. Facultatif : téléchargez le traçage système avec le code corrigé :

  1. Trouvez les sections ItemTag, vous pouvez observer que la compilation est plus rapide et qu'elle utilise la même section racine Compose:recompose.

219cd2e961defd1.jpeg

Une situation similaire peut se produire avec d'autres conteneurs utilisant un composable SubcomposeLayout, par exemple un composable BoxWithConstraints. Il peut répartir la création des éléments sur les sections Compose:recompose, ce qui n'apparaît pas forcément directement sous forme d'un frame saccadé, mais qui peut être visible pour l'utilisateur. Si vous le pouvez, essayez d'éviter un composable BoxWithConstraints dans chaque élément, car il pourrait n'être nécessaire que lorsque vous composez une interface utilisateur différente en fonction de l'espace disponible.

Dans cette section, vous avez appris à corriger les compositions qui prennent trop de temps.

8. Comparer les résultats avec ceux du benchmark initial

Maintenant que vous avez fini d'optimiser les performances de l'écran, vous devez comparer les résultats du benchmark avec les résultats initiaux.

  1. Ouvrez Test History (Historique de tests) dans le volet d'exécution d'Android Studio 667294bf641c8fc2.png
  2. Sélectionnez l'exécution la plus ancienne qui se rapporte au benchmark initial sans aucune modification et comparez les mesures frameDurationCpuMs et frameOverrunMs. Un résultat semblable au tableau suivant doit s'afficher :

Avant

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0
  1. Sélectionnez l'exécution la plus récente qui se rapporte au benchmark avec toutes les optimisations. Un résultat semblable au tableau suivant doit s'afficher :

Après

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min   2.9,   median   2.9,   max   2.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.4,   median   3.4,   max   3.4
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.1,   median   1.1,   max   1.1
frameDurationCpuMs                  P50    4.3,   P90    7.7,   P95    8.8,   P99   33.1
frameOverrunMs                      P50  -11.4,   P90   -8.3,   P95   -7.3,   P99   41.8
Traces: Iteration 0

Si vous vérifiez spécifiquement la ligne frameOverrunMs, vous pouvez voir que tous les percentiles se sont améliorés :

P50

P90

P95

P99

avant

-4.2

-3.5

-3.2

74.9

après

-11.4

-8.3

-7.3

41.8

amélioration

171 %

137 %

128 %

44 %

Dans la section suivante, vous apprendrez à corriger une composition qui se produit trop souvent.

9. Éviter les recompositions inutiles

Compose compte trois phases :

  • Composition : détermine ce qu'il faut montrer en construisant un arbre de composables.
  • Layout : prend ce même arbre et détermine où les composables apparaîtront à l'écran.
  • Drawing : dessine les composables à l'écran.

L'ordre de ces phases est généralement le même, ce qui permet aux données de circuler dans un sens, de la composition à la mise en page, afin de générer un frame UI.

2147ae29192a1556.png

BoxWithConstraints, les mises en page différées (par exemple, LazyColumn ou LazyVerticalGrid) et toutes les mises en page basées sur les composables SubcomposeLayout sont des exceptions notables. En effet, dans celles-ci, la composition des éléments enfant dépend des phases de mise en page des éléments parent.

Généralement, la composition est la phase la plus coûteuse à exécuter, car c'est celle où il y a le plus de travail à faire et où l'on peut également provoquer la recomposition d'autres composables non liés.

La plupart des frames suivent les trois phases, mais Compose peut faire disparaître l'une d'entre elles s'il n'y a pas de travail à faire. Vous pouvez tirer parti de cette capacité pour améliorer les performances de votre application.

Différer les phases de composition à l'aide de modificateurs lambda

Les fonctions composables sont exécutées durant la phase de composition. Pour permettre au code d'être exécuté à un moment différent, vous pouvez le fournir sous la forme d'une fonction lambda.

Pour cela, procédez comme suit :

  1. Ouvrez le fichier PhasesComposeLogo.kt.
  2. Accédez à l'écran Task 2 dans l'application. Vous voyez un logo qui rebondit sur le bord de l'écran.
  3. Ouvrez l'outil d'inspection de la mise en page et inspectez le nombre de recomposition. Vous constatez que le nombre de recompositions augmente rapidement.

a9e52e8ccf0d31c1.png

  1. Facultatif : localisez le fichier PhasesComposeLogoBenchmark.kt et exécutez-le pour récupérer la trace système et voir la composition de la section de trace PhasesComposeLogo qui se produit à chaque frame. Les recompositions sont représentées dans une trace par des sections répétées portant le même nom.

4b6e72578c89b2c1.jpeg 7036a895a31138d3.png

  1. Si nécessaire, fermez le profileur et l'outil d'inspection de la mise en page, puis revenez au code. Repérez le composable PhaseComposeLogo qui ressemble à ce qui suit :
@Composable
fun PhasesComposeLogo() = trace("PhasesComposeLogo") {
    val logo = painterResource(id = R.drawable.compose_logo)
    var size by remember { mutableStateOf(IntSize.Zero) }
    val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize)

    Box(
        modifier = Modifier
            .fillMaxSize()
            .onPlaced {
                size = it.size
            }
    ) {
        with(LocalDensity.current) {
            Image(
                painter = logo,
                contentDescription = "logo",
                modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp())
            )
        }
    }
}

Le composable logoPosition contient une logique qui modifie son état à chaque frame et se présente comme suit :

@Composable
fun logoPosition(size: IntSize, logoSize: Size): State<IntOffset> =
    produceState(initialValue = IntOffset.Zero, size, logoSize) {
        if (size == IntSize.Zero) {
            this.value = IntOffset.Zero
            return@produceState
        }

        var xDirection = 1
        var yDirection = 1

        while (true) {
            withFrameMillis {
                value += IntOffset(x = MOVE_SPEED * xDirection, y = MOVE_SPEED * yDirection)

                if (value.x <= 0 || value.x >= size.width - logoSize.width) {
                    xDirection *= -1
                }

                if (value.y <= 0 || value.y >= size.height - logoSize.height) {
                    yDirection *= -1
                }
            }
        }
    }

L'état est lu dans le composable PhasesComposeLogo avec le modificateur Modifier.offset(x.dp, y.dp), ce qui signifie qu'il est lu lors de la composition.

Ce modificateur explique pourquoi l'application se recompose à chaque frame de l'animation. Dans ce cas, il existe une alternative simple : le modificateur Offset basé sur le lambda.

  1. Mettez à jour le composable Image pour utiliser le modificateur Modifier.offset, qui accepte un lambda renvoyant l'objet IntOffset, comme dans l'extrait suivant :
Image(
  painter = logo,
  contentDescription = "logo",
  modifier = Modifier.offset { IntOffset(logoPosition.x,  logoPosition.y) }
)
  1. Exécutez à nouveau l'application et vérifiez l'outil d'inspection de la mise en page. Vous voyez que l'animation ne génère plus de recomposition.

Rappelez-vous que vous ne devriez pas avoir à recomposer uniquement pour ajuster la mise en page d'un écran, en particulier pendant le défilement, qui produit des images saccadées. La recomposition qui se produit pendant le défilement est presque toujours inutile et devrait être évitée.

Autres modificateurs lambda

Le modificateur Modifier.offset n'est pas le seul modificateur lambda. Dans le tableau suivant, vous pouvez voir les modificateurs courants qui se recomposent à chaque fois et qui peuvent être remplacés par leurs alternatives différées lorsqu'ils transmettent une valeur d'état qui change fréquemment :

Modificateur commun

Alternative différée

.background(color)

.drawBehind { drawRect(color) }

.offset(0.dp, y)

.offset { IntOffset(0, y.roundToPx()) }

.alpha(a).rotate(r).scale(s)

.graphicsLayer { alpha = a; rotationZ = r; scaleX = s; scaleY = s}

10. Différer les phases de Compose avec une mise en page personnalisée

L'utilisation d'un modificateur basé sur un lambda est souvent le moyen le plus simple d'éviter d'invalider la composition, mais il arrive qu'il n'existe pas de modificateur basé sur un lambda qui fasse ce dont vous avez besoin. Dans ces cas, vous pouvez directement implémenter une mise en page personnalisée ou même un composable Canvas pour passer directement à la phase de dessin. Les lectures de l'état de composition effectuées à l'intérieur d'une mise en page personnalisée n'invalident que la mise en page et ignorent la recomposition. En règle générale, si vous souhaitez uniquement ajuster la mise en page ou la taille, mais sans ajouter ou supprimer de composables, vous pouvez souvent obtenir l'effet désiré sans invalider la composition.

Pour cela, procédez comme suit :

  1. Ouvrez le fichier PhasesAnimatedShape.kt et exécutez l'application.
  2. Accédez à l'écran Task 3. Cet écran contient une forme qui change de taille lorsque vous cliquez sur un bouton. La valeur de la taille est animée à l'aide de l'API Animation animateDpAsState dans Compose.

51dc23231ebd5f1a.gif

  1. Ouvrez l'outil d'inspection de la mise en page.
  2. Cliquez sur Toggle size (Modifier la taille).
  3. Observez que la forme se recompose à chaque frame de l'animation.

63d597a98fca1133.png

Le composable MyShape prend l'objet size comme paramètre, qui est une lecture d'état. Cela signifie que lorsque l'objet change de size, le composable PhasesAnimatedShape (le champ de recomposition le plus proche) est recomposé et, par la suite, le composable MyShape est lui aussi recomposé parce que ses entrées ont changé.

Pour passer la recomposition, procédez comme suit :

  1. Remplacez le paramètre size par une fonction lambda afin que les modifications apportées à la taille n'entraînent pas directement la recomposition du composable MyShape :
@Composable
fun MyShape(
    size: () -> Dp,
    modifier: Modifier = Modifier
) {
  // ...
  1. Mettez à jour le site d'appel dans le composable PhasesAnimatedShape pour utiliser la fonction lambda :
MyShape(size = { size }, modifier = Modifier.align(Alignment.Center))

Remplacer le paramètre size par un lambda retarde la lecture de l'état. Désormais, la lecture se produit lorsque le lambda est invoqué.

  1. Remplacez le corps du composable MyShape par ce qui suit :
Box(
    modifier = modifier
        .background(color = Purple80, shape = CircleShape)
        .layout { measurable, _ ->
            val sizePx = size()
                .roundToPx()
                .coerceAtLeast(0)

            val constraints = Constraints.fixed(
                width = sizePx,
                height = sizePx,
            )

            val placeable = measurable.measure(constraints)
            layout(sizePx, sizePx) {
                placeable.place(0, 0)
            }
        }
)

Sur la première ligne du lambda de mesure du modificateur layout, vous pouvez voir que le lambda size est invoqué. Il se trouve à l'intérieur du modificateur layout, de sorte que seule la mise en page, et non la composition, soit invalidée.

  1. Exécutez à nouveau l'application, accédez à l'écran Task 3, puis ouvrez l'outil d'inspection de la mise en page.
  2. Cliquez sur Toggle Size (Modifier la taille) et observez que la taille de la forme s'anime de la même manière qu'auparavant, mais que le composable MyShape ne se recompose pas.

11. Empêcher les recompositions avec des classes stables

Compose génère un code qui peut ignorer l'exécution du composable si tous ses paramètres d'entrée sont stables et n'ont pas changé depuis la composition précédente. Un type est stable s'il est immuable ou s'il est possible pour le moteur de Compose de savoir si sa valeur a changé entre deux recompositions.

Si le moteur de Compose n'est pas sûr qu'un composable soit stable, il le traitera comme instable et ne générera pas la logique de code permettant d'éviter la recomposition, ce qui signifie que le composable se recomposera à chaque fois. Cela peut se produire lorsqu'une classe n'est pas un type primitif et que l'une des situations suivantes se produit :

  • Il s'agit d'une classe modifiable. Par exemple, elle contient une propriété modifiable.
  • Il s'agit d'une classe définie dans un module Gradle qui n'utilise pas Compose. Ces modules ne dépendent pas du compilateur Compose.
  • Il s'agit d'une classe contenant une propriété modifiable.

Ce comportement peut s'avérer indésirable dans certains cas, où il entraîne des problèmes de performances, et peut être modifié en procédant comme suit :

  • Activez le mode de désactivation renforcée (ou "skipping mode")
  • Annotez le paramètre avec @Immutable ou @Stable.
  • Ajoutez la classe au fichier de configuration de la stabilité.

Pour en savoir plus sur la stabilité, consultez la documentation.

Dans cette tâche, vous disposez d'une liste d'éléments qui peuvent être ajoutés, supprimés ou vérifiés, et vous devez vous assurer que les éléments ne se recomposent pas inutilement. Il existe deux types d'objets : ceux qui sont recréés à chaque fois et ceux qui ne le sont pas.

Les éléments qui sont recréés à chaque fois sont ici une simulation du cas d'utilisation réel dans lequel les données proviennent d'une base de données locale (par exemple Room ou sqlDelight) ou d'une source de données distante (telles que les requêtes API ou les entités Firestore), et renvoient une nouvelle instance de l'objet à chaque fois qu'il y a un changement.

Plusieurs composables sont liés à un modificateur Modifier.recomposeHighlighter(), que vous pouvez trouver dans notre dépôt GitHub. Ce modificateur affiche une bordure chaque fois qu'un composable est recomposé et peut servir de solution temporaire alternative à l'outil d'inspection de la mise en page.

127f2e4a2fc1a381.gif

Activer le mode de désactivation renforcée

Le compilateur Jetpack Compose 1.5.4 et versions ultérieures propose une option permettant d'activer le mode de désactivation renforcée, ce qui signifie que même les composables avec des paramètres instables peuvent générer un code de saut. Ce mode devrait permettre de réduire radicalement la quantité de composables non désactivables dans votre projet, améliorant ainsi les performances sans aucune modification du code.

Pour les paramètres instables, la logique de désactivation est comparée à l'égalité des instances, ce qui signifie que le paramètre serait ignoré si la même instance était transmise au composable que dans le cas précédent. En revanche, les paramètres stables utilisent l'égalité structurelle (en appelant la méthode Object.equals()) pour déterminer la logique de désactivation.

En plus d'ignorer la logique, le mode de désactivation renforcée se souvient automatiquement des lambda qui se trouvent à l'intérieur d'une fonction composable. Cela signifie que vous n'avez pas besoin d'un appel remember pour encapsuler une fonction lambda, qui appellerait par exemple une méthode ViewModel.

Ce mode peut être activé depuis un module Gradle.

Pour l'activer, procédez comme suit :

  1. Ouvrez le fichier build.gradle.kts de l'application.
  2. Mettez à jour le bloc composeCompiler avec l'extrait suivant :
composeCompiler {
    // Not required in Kotlin 2.0 final release
    suppressKotlinVersionCompatibilityCheck = "2.0.0-RC1"

    // This settings enables strong-skipping mode for all module in this project.
    // As an effect, Compose can skip a composable even if it's unstable by comparing it's instance equality (===).
    enableExperimentalStrongSkippingMode = true
}

L'argument du compilateur experimentalStrongSkipping est ainsi ajouté au module Gradle.

  1. Cliquez surb8a9619d159a7d8e.png Sync project with Gradle files (Synchroniser le projet avec les fichiers Gradle).
  2. Recompilez le projet.
  3. Ouvrez l'écran Task 5, vous pouvez remarquer que les éléments qui utilisent l'égalité structurelle sont marqués d'une icône EQU et qu'ils ne se recomposent pas lorsque vous interagissez avec la liste des éléments.

1de2fd2c42a1f04f.gif

Cependant, d'autres types d'éléments continuent à être recomposés. Vous vous occuperez de ce problème à l'étape suivante.

Corriger la stabilité grâce aux annotations

Comme indiqué précédemment, si le mode désactivation forcée est activé, un composable ignorera son exécution si le paramètre a la même instance que dans la composition précédente. Ce n'est toutefois pas le cas dans les situations où, à chaque modification, une nouvelle instance de la classe instable est fournie.

Dans votre cas, la classe StabilityItem est instable, car elle contient une propriété LocalDateTime instable.

Pour corriger la stabilité de cette classe, procédez comme suit :

  1. Accédez au fichier StabilityViewModel.kt.
  2. Localisez la classe StabilityItem et annotez-la avec @Immutable :
// TODO Codelab task: make this class Stable
@Immutable
data class StabilityItem(
    val id: Int,
    val type: StabilityItemType,
    val name: String,
    val checked: Boolean,
    val created: LocalDateTime
)
  1. Recompilez l'application.
  2. Accédez à l'écran Task 5 et observez qu'aucun des éléments de la liste n'est recomposé.

938aad77b78f7590.gif

Celle classe utilise désormais l'égalité structurelle pour vérifier si elle a changé par rapport à la composition précédente et ainsi éviter une recomposition.

Le composable qui se réfère à la date de la dernière modification est toujours présent, et il continue à se recomposer indépendamment de ce que vous avez fait jusqu'à présent.

Corriger la stabilité grâce au fichier de configuration

L'approche précédente fonctionne bien pour les classes qui font partie de votre codebase. Cependant, les classes qui sont hors de votre portée, telles que les classes de bibliothèques tierces ou celle de la bibliothèque standard, ne peuvent pas être modifiées.

Vous pouvez activer un fichier de configuration de la stabilité qui définit les classes (avec d'éventuels caractères génériques) qui seront considérées comme stables.

Pour ce faire, procédez comme suit :

  1. Accédez au fichier build.gradle.kts de l'application.
  2. Ajoutez l'option stabilityConfigurationFile au bloc composeCompiler :
composeCompiler {
    ...

    stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
}
  1. Synchronisez le projet avec les fichiers Gradle.
  2. Ouvrez le fichier stability_config.conf dans le dossier racine de ce projet à côté du fichier README.md.
  3. Ajoutez ceci :
// TODO Codelab task: Make a java.time.LocalDate class stable.
java.time.LocalDate
  1. Recompilez l'application. Si la date reste inchangée, la classe LocalDateTime n'entraînera pas la recomposition du composable Latest change was YYYY-MM-DD (Dernière modification le JJ-MM-AAAA).

332ab0b2c91617f2.gif

Dans votre application, vous pouvez étendre le fichier pour qu'il contienne des modèles, de sorte que vous n'ayez pas à écrire toutes les classes qui doivent être traitées comme stables. Dans votre cas, vous pouvez donc utiliser le caractère générique java.time.*, qui traitera toutes les classes du package comme stables, telles que Instant, LocalDateTime, ZoneId, and d'autres classes de "java.time".

En suivant ces étapes, rien sur cet écran ne se recompose à l'exception de l'élément qui a été ajouté ou avec lequel vous avez interagi, ce qui est un comportement attendu.

12. Félicitations

Félicitations, vous avez optimisé les performances d'une application Compose ! Même si vous n'avez pu voir que quelques problèmes de performances que vous pourriez rencontrer dans votre application, vous avez appris à chercher d'autres potentiels problèmes et à les résoudre.

Et ensuite ?

Si vous n'avez pas créé de profil de référence pour votre application, nous vous recommandons vivement de le faire.

Vous pouvez suivre l'atelier de programmation Améliorer les performances de l'application avec les profils de référence. Pour plus d'informations sur la mise en place de benchmarks, consultez l'atelier Inspecter les performances de l'application avec Macrobenchmark.

En savoir plus