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.
- 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.
- Le dernier écran contient des éléments instables. Ici, vous stabilisez les éléments à l'aide de différentes techniques.
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
- La dernière version stable d'Android Studio
- Un appareil Android physique avec Android 6 (niveau d'API 23) ou version ultérieure
2. Configuration
Pour commencer, procédez comme suit :
- 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 :
- 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.
- 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 :
- Établissez les performances initiales en les mesurant.
- Déterminez la cause du problème.
- Modifiez le code d'après vos observations.
- Évaluez les performances et comparez-les aux performances initiales.
- 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 :
Pour plus d'informations sur les profils de référence, consultez les ressources suivantes :
- Documentation sur les profils de référence
- Atelier de programmation : améliorer les performances de l'application avec les profils de référence
É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 :
- Atelier de programmation : inspecter les performances de l'application avec Macrobenchmark
- Inspecter les performances – MAD Skills
- Documentation sur l'écriture d'un macrobenchmark
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.
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 :
- 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.
- 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")
- Ajoutez l'argument d'instrumentation
androidx.benchmark.fullTracing.enable=true
au fichierbuild.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 :
- Ouvrez le fichier
AccelerateHeavyScreenBenchmark.kt
. - Exécutez le benchmark depuis la gouttière située en regard de la classe de benchmark :
Ce benchmark fait défiler l'écran Task 1 et capture le temps de rendu et
les sections de trace personnalisées.
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.
- Accédez au site Web Perfetto qui charge le tableau de bord de l'outil.
- 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.
- Faites glisser le fichier
AccelerateHeavyScreenBenchmark_...iter000...perfetto-trace
vers l'interface utilisateur de Perfetto et attendez que le fichier de trace soit chargé. - 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 :
- 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.
- 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.
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 :
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.
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 :
- Ouvrez la trace système que vous avez enregistrée à l'étape précédente.
- 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 :
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.
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 :
- Accédez au fichier
AccelerateHeavyScreen.kt
. - 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.
- Modifiez le drawable en
R.drawable.placeholder_vector
:
@Composable
fun imagePlaceholder() =
trace("ImagePlaceholder") { painterResource(R.drawable.placeholder_vector) }
- Réexécutez le test
AccelerateHeavyScreenBenchmark
, ce qui recompile l'application et évalue à nouveau la trace système. - Faites glisser la trace système vers le tableau de bord Perfetto.
Vous pouvez également télécharger la trace :
- Cherchez la section de trace
ImagePlaceholder
, qui vous montre directement la partie améliorée.
- Vous pouvez constater que la fonction
ImagePlaceholder
ne bloque plus autant le thread principal.
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.
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 :
- Ouvrez le fichier
AccelerateHeavyScreen.kt
. - Localisez le composable
PublishedText
. Ce composable formate une date avec le fuseau horaire actuel et enregistre un objetBroadcastReceiver
qui garde la trace des changements de fuseau horaire. Il contient une variable d'étatcurrentTimeZone
dont la valeur initiale est le fuseau horaire par défaut du système, ainsi qu'unDisposableEffect
qui enregistre un broadcast receiver pour les changements de fuseau horaire. Enfin, ce composable affiche une date formatée avecText
.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 lambdaonDispose
. La partie problématique, cependant, est que le code à l'intérieur deDisposableEffect
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
)
}
- Encapsulez le
context.registerReceiver
dans un appeltrace
pour vous assurer qu'il est bien la cause desbinder 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.
- Obtenez un champ d'application lié au cycle de vie du composable
val scope = rememberCoroutineScope()
. - À 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 actuelcurrentTimeZone
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.
- Définissez la composition locale avec le fuseau horaire par défaut du système :
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
- Mettez à jour le composable
ProvideCurrentTimeZone
qui prend un lambdacontent
pour transmettre le fuseau horaire actuel :
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
var currentTimeZone = TODO()
CompositionLocalProvider(
value = LocalTimeZone provides currentTimeZone,
content = content,
)
}
- Sortez le
DisposableEffect
du composablePublishedText
dans le nouveau pour l'y hisser, et remplacez lecurrentTimeZone
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,
)
}
- Encapsulez un composable dans lequel vous voulez que la composition locale soit valide avec
ProvideCurrentTimeZone
. Vous pouvez encapsuler l'ensemble duAccelerateHeavyScreen
, 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))
}
}
}
}
- 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 viaLocalTimeZone.current
:
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
Text(
text = published.format(LocalTimeZone.current),
style = MaterialTheme.typography.labelMedium,
modifier = modifier
)
}
- Relancez le benchmark, qui compile l'application.
Vous pouvez également télécharger la trace système avec le code corrigé :
- Faites glisser le fichier de trace vers le tableau de bord Perfetto. Toutes les sections
binder transactions
ont disparu du fil principal. - 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
) :
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 :
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.
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.
Pour régler ce problème, procédez comme suit :
- Accédez au fichier
AccelerateHeavyScreen.kt
et localisez le composableItemTags
. - Modifiez l'implémentation
LazyRow
en un composableRow
qui effectue une itération vers la listetags
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) }
}
}
- Relancez le benchmark, qui compile également l'application.
- Facultatif : téléchargez le traçage système avec le code corrigé :
- Trouvez les sections
ItemTag
, vous pouvez observer que la compilation est plus rapide et qu'elle utilise la même section racineCompose:recompose
.
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.
- Ouvrez Test History (Historique de tests) dans le volet d'exécution d'Android Studio
- Sélectionnez l'exécution la plus ancienne qui se rapporte au benchmark initial sans aucune modification et comparez les mesures
frameDurationCpuMs
etframeOverrunMs
. 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
- 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.
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 :
- Ouvrez le fichier
PhasesComposeLogo.kt
. - Accédez à l'écran Task 2 dans l'application. Vous voyez un logo qui rebondit sur le bord de l'écran.
- 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.
- 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 tracePhasesComposeLogo
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.
- 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.
- Mettez à jour le composable
Image
pour utiliser le modificateurModifier.offset
, qui accepte un lambda renvoyant l'objetIntOffset
, comme dans l'extrait suivant :
Image(
painter = logo,
contentDescription = "logo",
modifier = Modifier.offset { IntOffset(logoPosition.x, logoPosition.y) }
)
- 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 |
|
|
|
|
|
|
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 :
- Ouvrez le fichier
PhasesAnimatedShape.kt
et exécutez l'application. - 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.
- Ouvrez l'outil d'inspection de la mise en page.
- Cliquez sur Toggle size (Modifier la taille).
- Observez que la forme se recompose à chaque frame de l'animation.
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 :
- 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 composableMyShape
:
@Composable
fun MyShape(
size: () -> Dp,
modifier: Modifier = Modifier
) {
// ...
- 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é.
- 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.
- Exécutez à nouveau l'application, accédez à l'écran Task 3, puis ouvrez l'outil d'inspection de la mise en page.
- 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.
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 :
- Ouvrez le fichier
build.gradle.kts
de l'application. - 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.
- Cliquez sur Sync project with Gradle files (Synchroniser le projet avec les fichiers Gradle).
- Recompilez le projet.
- 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.
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 :
- Accédez au fichier
StabilityViewModel.kt
. - 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
)
- Recompilez l'application.
- Accédez à l'écran Task 5 et observez qu'aucun des éléments de la liste n'est recomposé.
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 :
- Accédez au fichier
build.gradle.kts
de l'application. - Ajoutez l'option
stabilityConfigurationFile
au bloccomposeCompiler
:
composeCompiler {
...
stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
}
- Synchronisez le projet avec les fichiers Gradle.
- Ouvrez le fichier
stability_config.conf
dans le dossier racine de ce projet à côté du fichierREADME.md
. - Ajoutez ceci :
// TODO Codelab task: Make a java.time.LocalDate class stable.
java.time.LocalDate
- 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).
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.