Suivez les bonnes pratiques

Vous pouvez rencontrer les pièges courants de Compose. Ces erreurs peuvent vous donner qui semble fonctionner assez bien, mais peut nuire aux performances de votre interface utilisateur. Suivre le meilleur pour optimiser votre application sur Compose.

Utiliser remember pour réduire les calculs coûteux

Les fonctions composables peuvent s'exécuter très fréquemment, aussi souvent que pour chaque frame d'une animation. Par conséquent, nous vous conseillons de réduire le nombre de calculs au minimum dans le corps de votre composable.

Une technique importante consiste à stocker les résultats des calculs avec remember Ainsi, le calcul est exécuté une seule fois, et vous pouvez récupérer des résultats chaque fois qu'ils sont nécessaires.

Voici un exemple de code qui affiche une liste triée de noms de manière très coûteuse:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

Chaque fois que ContactsList est recomposé, la liste de contacts complète est triée encore une fois, même si la liste n'a pas changé. Si l'utilisateur fait défiler la liste, le composable est recomposé lorsqu'une nouvelle ligne apparaît.

Pour résoudre ce problème, triez la liste en dehors de LazyColumn et stockez la liste triée avec remember :

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

Désormais, la liste est triée une fois, lors de la première composition de ContactList. Si les contacts ou le comparateur changent, la liste triée est générée une nouvelle fois. Sinon, le composable peut continuer à utiliser la liste triée en cache.

Utiliser des clés de mise en page différée

Les mises en page différées réutilisent efficacement les éléments, en les regénérant ou en les recompilant uniquement quand ils le doivent. Toutefois, vous pouvez optimiser les mises en page différées la recomposition.

Supposons qu'une opération utilisateur entraîne le déplacement d'un élément de la liste. Par exemple : supposons que vous affichiez une liste de notes triées par date de modification avec le plus grand nombre note récemment modifiée en haut.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Il y a un problème avec ce code. Supposons que la note inférieure ait été modifiée. Il s'agit désormais de la dernière note modifiée. Elle est donc placée en haut de la liste, et toutes les autres notes sont déplacées d'une ligne vers le bas.

Sans votre aide, Compose ne comprend pas que les éléments inchangés sont simplement déplacés dans la liste. Compose considère plutôt l'ancien "élément 2" a été supprimé et une nouvelle a été créée pour l'élément 3, l'élément 4 et tout le bas. Résultat : Compose recompose chaque élément de la liste, même si un seul d'entre eux réellement modifié.

La solution consiste à fournir des clés d'élément. Fournir une clé stable pour chaque élément permet à Compose d'éviter les recompositions inutiles. Dans ce cas, Compose que l'élément présent à l'emplacement 3 est le même élément qu'à l'emplacement 2. Comme aucune des données de cet élément n'a changé, il n'est pas nécessaire que Compose la recomposer.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

Utiliser derivedStateOf pour limiter les recompositions

L'un des risques liés à l'utilisation de l'état dans vos compositions est que, si l'état change rapidement, votre UI risque d'être recomposée plus que nécessaire. Par exemple : supposons que vous affichiez une liste déroulante. Vous examinez l'état de la liste pour déterminer qui est le premier élément visible de la liste:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Le problème est le suivant : si l'utilisateur fait défiler la liste, listState change constamment, au fur et à mesure que l'utilisateur la fait défiler. Cela signifie que la liste est constamment recomposée. Cependant, vous n'avez pas besoin de la recomposer aussi souvent. n'ont pas besoin de les recomposer tant qu'un nouvel élément n'est pas visible en bas. Cela représente beaucoup de calculs supplémentaires, ce qui nuit aux performances de votre UI.

La solution consiste à utiliser l'état dérivé. L'état dérivé vous permet d'indiquer à Compose quels changements d'état doivent déclencher une recomposition. Dans ce cas, indiquer que vous vous souciez du moment où le premier élément visible change. Quand cela change de valeur d'état, l'UI doit se recomposer, mais si l'utilisateur n'a pas encore si vous faites défiler la page pour placer un nouvel élément en haut, il n'est pas nécessaire de le recomposer.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Reporter les lectures le plus longtemps possible

Lorsqu'un problème de performances a été identifié, il peut être utile de reporter les lectures de l'état. Le report des lectures de l'état garantit que Compose réexécute le minimum de code lors de la recomposition. Par exemple, si l'état de votre UI est hissé en haut de l'arborescence composable et que vous lisez cet état dans un composable enfant, vous pouvez encapsuler la lecture d'état dans une fonction lambda. Ainsi, la lecture ne se produit que lorsqu'elle est réellement nécessaire. Pour référence, reportez-vous à l'implémentation dans le document Jetsnack application exemple. Jetsnack implémente un effet semblable à une barre d'outils qui se réduit sur l'écran détaillé. Pour comprendre pourquoi cette technique fonctionne, consultez l'article de blog Jetpack Compose: déboguer une recomposition

Pour obtenir cet effet, le composable Title a besoin du décalage de défilement. afin de s'ajuster à l'aide d'un Modifier. Voici une version simplifiée du Code Jetsnack avant l'optimisation:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Lorsque l'état de défilement change, Compose invalide le parent le plus proche. Champ d'application de la recomposition. Dans ce cas, le niveau d'accès le plus proche est SnackDetail. composable. Notez que Box est une fonction intégrée, et qu'il ne s'agit donc pas d'une recomposition. le champ d'application. Compose recompose donc SnackDetail et tous les composables qu'il contient SnackDetail Si vous modifiez votre code pour ne lire que l'état dans lequel vous l'utiliser, vous pouvez alors réduire le nombre d'éléments à recomposer.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Le paramètre de défilement est désormais un lambda. Cela signifie que Title peut toujours référencer l'état hissé, mais que la valeur n'est lue que dans Title, où elle est réellement nécessaire. Par conséquent, lorsque la valeur de défilement change, le champ d'application de recomposition le plus proche est à présent le composable Title. Compose n'a plus besoin de recomposer l'intégralité de Box.

C'est une nette amélioration, mais vous pouvez faire mieux. Vous devriez vous poser des questions si une recomposition est effectuée uniquement pour réagencer ou redessiner un composable. Dans ce cas, il vous suffit de modifier le décalage du composable Title, ce qui peut être fait dans la phase de mise en page.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

Auparavant, le code utilisait Modifier.offset(x: Dp, y: Dp), qui prend le offset en tant que paramètre. En passant à la version lambda du modificateur, vous pouvez vous assurer que la fonction lit l'état de défilement lors de la phase de mise en page. Ainsi, lorsque l'état de défilement change, Compose peut ignorer la phase de composition et passer directement à la phase de mise en page. Lorsque vous transmettez des variables d'état qui changent fréquemment à des modificateurs, utilisez les versions lambda des modificateurs dans la mesure du possible.

Voici un autre exemple de cette approche. Ce code n'a pas encore été optimisé :

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Ici, l'arrière-plan de la zone passe rapidement d'une couleur à l'autre. Cet état change donc très fréquemment. Le composable lit ensuite cet état dans le modificateur d'arrière-plan. Par conséquent, la zone doit recomposer sur chaque image, car la couleur change à chaque image.

Pour améliorer cela, utilisez un modificateur basé sur lambda, dans ce cas, drawBehind. Cela signifie que l'état des couleurs n'est lu que pendant la phase de dessin. Par conséquent, Compose peut ignorer complètement les phases de composition et de mise en page, lorsque la couleur Compose passe directement à la phase de dessin.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

Éviter les rétroécritures

Compose repose sur une hypothèse fondamentale : vous n'écrivez jamais dans un état déjà lu. Ce procédé de rétroécriture peut entraîner une recomposition sans fin à chaque image.

Le composable suivant montre un exemple de ce type d'erreur.

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

Ce code met à jour le nombre à la fin du composable après l'avoir lu sur le ligne précédente. Si vous exécutez ce code, vous constaterez qu'après avoir cliqué sur qui entraîne une recomposition, le compteur augmente rapidement une boucle infinie pendant que Compose recompose ce composable, voit une lecture d'état qui est obsolète, et planifie donc une autre recomposition.

Vous pouvez éviter les rétroécritures en n'écrivant jamais dans un état dans la composition. Si possible, écrivez toujours dans un état en réponse à un événement et dans un lambda, comme dans l'exemple onClick précédent.

Autres ressources