Résoudre les problèmes de stabilité

Lorsque vous êtes confronté à une classe instable qui entraîne des problèmes de performances, vous devez la rendre stable. Ce document décrit plusieurs techniques que vous pouvez utiliser à cet effet.

Rendre la classe immuable

Vous devez d'abord essayer de rendre une classe instable complètement immuable.

  • Immutable: indique un type pour lequel la valeur de n'importe quelle propriété ne peut jamais changer après la construction d'une instance de ce type, et toutes les méthodes sont référentielles en transparence.
    • Assurez-vous que toutes les propriétés de la classe sont à la fois val plutôt que var et de types immuables.
    • Les types primitifs tels que String, Int et Float sont toujours immuables.
    • Si cela est impossible, vous devez utiliser l'état Compose pour toutes les propriétés modifiables.
  • Stable: indique un type modifiable. L'environnement d'exécution Compose ne sait pas si des propriétés publiques ou des comportements de méthode du type peuvent générer des résultats différents d'un appel précédent.

Collections immuables

L'une des raisons courantes pour lesquelles Compose considère qu'une classe est instable est une collection. Comme indiqué sur la page Diagnostiquer les problèmes de stabilité, le compilateur Compose ne peut pas être complètement sûr que les collections telles que List, Map et Set sont réellement immuables et les marque donc comme instables.

Pour résoudre ce problème, vous pouvez utiliser des collections immuables. Le compilateur Compose est compatible avec les collections immuables Kotlinx. Ces collections sont immuables, et le compilateur Compose les traite comme tel. Cette bibliothèque étant encore en version alpha, attendez-vous à des modifications possibles de son API.

Considérez à nouveau cette classe instable du guide Diagnostiquer les problèmes de stabilité:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

Vous pouvez rendre tags stable à l'aide d'une collection immuable. Dans la classe, remplacez le type tags par ImmutableSet<String>:

data class Snack{
    …
    val tags: ImmutableSet<String> = persistentSetOf()
    …
}

Une fois cette opération effectuée, tous les paramètres de la classe sont immuables, et le compilateur Compose la marque comme stable.

Annoter avec Stable ou Immutable

Pour résoudre les problèmes de stabilité, vous pouvez annoter les classes instables avec @Stable ou @Immutable.

L'annotation d'une classe remplace ce que le compilateur déduira au sujet de votre classe. Il est semblable à l'opérateur Kotlin !!. Vous devez faire très attention à la façon dont vous utilisez ces annotations. Ignorer le comportement du compilateur peut entraîner des bugs imprévus, tels que l'échec de la recomposition de votre composable au moment prévu.

S'il est possible de rendre votre classe stable sans annotation, vous devez vous efforcer d'obtenir la stabilité de cette manière.

L'extrait de code suivant fournit un exemple minimal de classe de données annotée comme immuable:

@Immutable
data class Snack(
…
)

Que vous utilisiez l'annotation @Immutable ou @Stable, le compilateur Compose marque la classe Snack comme stable.

Classes annotées dans les collections

Prenons l'exemple d'un composable qui inclut un paramètre de type List<Snack>:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  …
  unstable snacks: List<Snack>
  …
)

Même si vous annotez Snack avec @Immutable, le compilateur Compose marque toujours le paramètre snacks dans HighlightedSnacks comme instable.

Les paramètres rencontrent le même problème que les classes en ce qui concerne les types de collections. Le compilateur Compose marque toujours un paramètre de type List comme instable, même s'il s'agit d'une collection de types stables.

Vous ne pouvez pas marquer un paramètre individuel comme stable ni annoter un composable pour qu'il soit toujours désactivable. Il y a plusieurs chemins vers l'avant.

Il existe plusieurs moyens de contourner le problème des collections instables. Les sous-sections suivantes décrivent ces différentes approches.

Fichier de configuration

Si vous êtes satisfait du contrat de stabilité dans votre codebase, vous pouvez choisir de considérer les collections Kotlin comme stables en ajoutant kotlin.collections.* à votre fichier de configuration de stabilité.

Collection immuable

Pour une sécurité immuable de l'immuabilité lors de la compilation, vous pouvez utiliser une collection immuable Kotlin au lieu de List.

@Composable
private fun HighlightedSnacks(
    …
    snacks: ImmutableList<Snack>,
    …
)

Wrapper

Si vous ne pouvez pas utiliser une collection immuable, vous pouvez créer la vôtre. Pour ce faire, encapsulez List dans une classe stable annotée. Un wrapper générique est probablement le meilleur choix pour cela, en fonction de vos besoins.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

Vous pouvez ensuite l'utiliser comme type de paramètre dans votre composable.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

Solution

Après avoir adopté l'une de ces approches, le compilateur Compose marque maintenant le composable HighlightedSnacks comme skippable et restartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Lors de la recomposition, Compose peut désormais ignorer HighlightedSnacks si aucune de ses entrées n'a changé.

Fichier de configuration de stabilité

À partir de la version 1.5.5 de Compose Compiler, un fichier de configuration des classes à considérer comme stables peut être fourni au moment de la compilation. Cela permet de considérer comme stables les classes que vous ne contrôlez pas, telles que les classes de bibliothèques standards telles que LocalDateTime.

Le fichier de configuration est un fichier au format texte brut avec une classe par ligne. Les commentaires ainsi que les caractères génériques simples et doubles sont acceptés. Voici un exemple de configuration:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Pour activer cette fonctionnalité, transmettez le chemin d'accès du fichier de configuration aux options du compilateur Compose.

Groovy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Comme le compilateur Compose s'exécute séparément sur chaque module de votre projet, vous pouvez fournir différentes configurations à différents modules si nécessaire. Vous pouvez également disposer d'une configuration au niveau racine de votre projet et transmettre ce chemin d'accès à chaque module.

Plusieurs modules

L'architecture multimodule est un autre problème courant. Le compilateur Compose ne peut déterminer si une classe est stable que si tous les types non primitifs auxquels elle fait référence sont explicitement marqués comme stables ou dans un module également créé avec le compilateur Compose.

Si votre couche de données se trouve dans un module distinct de la couche d'interface utilisateur (méthode recommandée), vous pouvez rencontrer un problème.

Solution

Pour résoudre ce problème, vous pouvez adopter l'une des approches suivantes:

  1. Ajoutez les classes à votre fichier de configuration du compilateur.
  2. Activez le compilateur Compose sur vos modules de couche de données ou taguez vos classes avec @Stable ou @Immutable, le cas échéant.
    • Cela implique d'ajouter une dépendance Compose à votre couche de données. Cependant, il s'agit uniquement de la dépendance pour l'environnement d'exécution Compose, et non pour Compose-UI.
  3. Dans votre module d'interface utilisateur, encapsulez vos classes de couche de données dans des classes de wrapper spécifiques à l'interface utilisateur.

Le même problème se produit également lorsque vous utilisez des bibliothèques externes si elles n'utilisent pas le compilateur Compose.

Tous les composables ne doivent pas être ignorables

Lorsque vous tentez de résoudre les problèmes de stabilité, vous ne devez pas essayer de rendre tous les composables ignorables. Si vous tentez de le faire, vous risquez d'obtenir une optimisation prématurée qui entraînera plus de problèmes qu'elle ne permet de les résoudre.

Dans de nombreuses situations, l'option "désactivable" n'a pas d'intérêt réel et peut compliquer la gestion du code. Par exemple :

  • Un composable qui n'est pas recomposé souvent, voire pas du tout.
  • Un composable qui, en soi, appelle simplement des composables désactivables.
  • Composable avec un grand nombre de paramètres et des implémentations "égal" coûteuses. Dans ce cas, le coût de vérification de la modification d'un paramètre peut l'emporter sur le coût d'une recomposition bon marché.

Lorsqu'un composable est ignorable, cela ajoute une petite surcharge qui peut ne pas en valoir la peine. Vous pouvez même annoter votre composable pour qu'il soit non redémarrable si vous déterminez qu'un redémarrage nécessite plus d'efforts qu'il n'en vaut la peine.