Combiner des rappels d'état Compose avec RememberObserver et RetainObserver

Dans Jetpack Compose, un objet peut implémenter RememberObserver pour recevoir des rappels lorsqu'il est utilisé avec remember afin de savoir quand il commence et cesse d'être mémorisé dans la hiérarchie de composition. De même, vous pouvez utiliser RetainObserver pour recevoir des informations sur l'état d'un objet utilisé avec retain.

Pour les objets qui utilisent ces informations sur le cycle de vie à partir de la hiérarchie de composition, nous vous recommandons de suivre quelques bonnes pratiques pour vérifier que vos objets se comportent correctement sur la plate-forme et se protègent contre les utilisations abusives. Plus précisément, utilisez les rappels onRemembered (ou onRetained) pour lancer le travail au lieu du constructeur, annulez tout le travail lorsque les objets ne sont plus mémorisés ni conservés, et évitez de divulguer les implémentations de RememberObserver et RetainObserver pour éviter les appels accidentels. La section suivante explique ces recommandations plus en détail.

Initialisation et nettoyage avec RememberObserver et RetainObserver

Le guide "Penser en Compose" décrit le modèle mental derrière la composition. Lorsque vous travaillez avec RememberObserver et RetainObserver, il est important de garder à l'esprit deux comportements de composition :

  • La recomposition est optimiste et peut être annulée
  • Aucune des fonctions composables ne devrait avoir d'effet secondaire.

Exécuter les effets secondaires de l'initialisation pendant onRemembered ou onRetained, et non pendant la construction

Lorsque des objets sont mémorisés ou conservés, le lambda de calcul s'exécute dans le cadre de la composition. Pour les mêmes raisons que celles qui vous empêchent d'effectuer un effet secondaire ou de lancer une coroutine pendant la composition, vous ne devez pas non plus effectuer d'effets secondaires dans le lambda de calcul transmis à remember, retain et leurs variantes. Cela inclut le constructeur des objets mémorisés ou conservés.

Au lieu de cela, lorsque vous implémentez RememberObserver ou RetainObserver, vérifiez que tous les effets et les tâches lancées sont distribués dans le rappel onRemembered. Cela offre le même timing que les API SideEffect. Il garantit également que ces effets ne s'exécutent que lorsque la composition est appliquée, ce qui évite les jobs orphelins et les fuites de mémoire si une recomposition est abandonnée ou différée.

class MyComposeObject : RememberObserver {
    private val job = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.Main + job)

    init {
        // Not recommended: This will cause work to begin during composition instead of
        // with other effects. Move this into onRemembered().
        coroutineScope.launch { loadData() }
    }

    override fun onRemembered() {
        // Recommended: Move any cancellable or effect-driven work into the onRemembered
        // callback. If implementing RetainObserver, this should go in onRetained.
        coroutineScope.launch { loadData() }
    }

    private suspend fun loadData() { /* ... */ }

    // ...
}

Démontage en cas d'oubli, de suppression ou d'abandon

Pour éviter la fuite de ressources ou l'abandon de tâches en arrière-plan, les objets mémorisés doivent également être supprimés. Pour les objets qui implémentent RememberObserver, cela signifie que tout ce qui est initialisé dans onRemembered doit avoir un appel de libération correspondant dans onForgotten.

Étant donné qu'une composition peut être annulée, les objets qui implémentent RememberObserver doivent également se nettoyer eux-mêmes s'ils sont abandonnés dans des compositions. Un objet est abandonné lorsqu'il est renvoyé par remember dans une composition qui est annulée ou échoue. (Cela se produit le plus souvent lorsque vous utilisez PausableComposition, et peut également se produire lorsque vous utilisez le rechargement à chaud avec l'outil d'aperçu composable d'Android Studio.)

Lorsqu'un objet mis en cache est abandonné, il ne reçoit que l'appel à onAbandoned (et aucun appel à onRemembered). Pour implémenter la méthode d'abandon, supprimez tout ce qui a été créé entre l'initialisation de l'objet et le moment où l'objet aurait reçu le rappel onRemembered.

class MyComposeObject : RememberObserver {
    private val job = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.Main + job)

    // ...

    override fun onForgotten() {
        // Cancel work launched from onRemembered. If implementing RetainObserver, onRetired
        // should cancel work launched from onRetained.
        job.cancel()
    }

    override fun onAbandoned() {
        // If any work was launched by the constructor as part of remembering the object,
        // you must cancel that work in this callback. For work done as part of the construction
        // during retain, this code should will appear in onUnused.
        job.cancel()
    }
}

Garder privées les implémentations de RememberObserver et RetainObserver

Lorsque vous écrivez des API publiques, soyez prudent lorsque vous étendez RememberObserver et RetainObserver pour créer des classes qui sont renvoyées publiquement. Il est possible qu'un utilisateur ne se souvienne pas de votre objet lorsque vous vous y attendez, ou qu'il s'en souvienne d'une manière différente de celle que vous aviez prévue. Pour cette raison, nous vous recommandons de ne pas exposer de constructeurs ni de fonctions de fabrique pour les objets qui implémentent RememberObserver ou RetainObserver. Notez que cela dépend du type d'exécution d'une classe, et non du type déclaré. Si vous vous souvenez qu'un objet qui implémente RememberObserver ou RetainObserver, mais qui est casté en Any, reçoit toujours des rappels.

Option déconseillée :

abstract class MyManager

// Not Recommended: Exposing a public constructor (even implicitly) for an object implementing
// RememberObserver can cause unexpected invocations if it is remembered multiple times.
class MyComposeManager : MyManager(), RememberObserver { ... }

// Not Recommended: The return type may be an implementation of RememberObserver and should be
// remembered explicitly.
fun createFoo(): MyManager = MyComposeManager()

Recommandations :

abstract class MyManager

class MyComposeManager : MyManager() {
    // Callers that construct this object must manually call initialize and teardown
    fun initialize() { /*...*/ }
    fun teardown() { /*...*/ }
}

@Composable
fun rememberMyManager(): MyManager {
    // Protect the RememberObserver implementation by never exposing it outside the library
    return remember {
        object : RememberObserver {
            val manager = MyComposeManager()
            override fun onRemembered() = manager.initialize()
            override fun onForgotten() = manager.teardown()
            override fun onAbandoned() { /* Nothing to do if manager hasn't initialized */ }
        }
    }.manager
}

Éléments à prendre en compte pour mémoriser des objets

En plus des recommandations précédentes concernant RememberObserver et RetainObserver, nous vous recommandons également d'être attentif et d'éviter de réenregistrer accidentellement des objets, à la fois pour les performances et l'exactitude. Les sections suivantes présentent plus en détail des scénarios de réapprentissage spécifiques et expliquent pourquoi ils doivent être évités.

Ne mémoriser les objets qu'une seule fois

Se souvenir à nouveau d'un objet peut être dangereux. Dans le meilleur des cas, vous risquez de gaspiller des ressources en mémorisant une valeur qui l'est déjà. Toutefois, si un objet implémente RememberObserver et qu'il est mémorisé deux fois de manière inattendue, il recevra plus de rappels que prévu. Cela peut poser problème, car la logique onRemembered et onForgotten s'exécutera deux fois, et la plupart des implémentations de RememberObserver ne sont pas compatibles avec ce cas. Si un deuxième appel "remember" se produit dans une portée différente qui a une durée de vie différente de celle du remember d'origine, de nombreuses implémentations de RememberObserver.onForgotten éliminent l'objet avant qu'il ne soit complètement utilisé.

val first: RememberObserver = rememberFoo()

// Not Recommended: Re-remembered `Foo` now gets double callbacks
val second = remember { first }

Ce conseil ne s'applique pas aux objets qui sont à nouveau mémorisés de manière transitive (c'est-à-dire aux objets mémorisés qui consomment un autre objet mémorisé). Il est courant d'écrire du code qui ressemble à ce qui suit, ce qui est autorisé, car un objet différent est mémorisé et ne provoque donc pas de doublement inattendu du rappel.

val foo: Foo = rememberFoo()

// Acceptable:
val bar: Bar = remember { Bar(foo) }

// Recommended key usage:
val barWithKey: Bar = remember(foo) { Bar(foo) }

Partir du principe que les arguments de la fonction sont déjà mémorisés

Une fonction ne doit se souvenir d'aucun de ses paramètres, car cela peut entraîner des invocations de rappel doubles pour RememberObserver et parce que ce n'est pas nécessaire. Si un paramètre d'entrée doit être mémorisé, vérifiez qu'il n'implémente pas RememberObserver ou demandez aux appelants de mémoriser leur argument.

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Not Recommended: Input should be remembered by the caller.
    val rememberedParameter = remember { parameter }
}

Cela ne s'applique pas aux objets mémorisés de manière transitive. Lorsque vous mémorisez un objet dérivé des arguments d'une fonction, pensez à le spécifier comme l'une des clés de remember :

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Acceptable:
    val derivedValue = remember { Bar(parameter) }

    // Also Acceptable:
    val derivedValueWithKey = remember(parameter) { Bar(parameter) }
}

Ne pas conserver un objet dont l'emplacement est déjà enregistré

Comme pour la réactivation d'un objet, vous devez éviter de conserver un objet mémorisé pour essayer de prolonger sa durée de vie. Cela découle du conseil donné dans Durée de vie des états : retain ne doit pas être utilisé avec des objets dont la durée de vie ne correspond pas à la durée de vie des offres de fidélisation. Étant donné que les objets remembered ont une durée de vie plus courte que les objets retained, vous ne devez pas conserver un objet mémorisé. Au lieu de cela, préférez conserver l'objet sur le site d'origine plutôt que de le mémoriser.