Comporre callback di stato con RememberObserver e RetainObserver

In Jetpack Compose, un oggetto può implementare RememberObserver per ricevere callback quando viene utilizzato con remember per sapere quando inizia e smette di essere memorizzato nella gerarchia di composizione. Allo stesso modo, puoi utilizzare RetainObserver per ricevere informazioni sullo stato di un oggetto utilizzato con retain.

Per gli oggetti che utilizzano queste informazioni sul ciclo di vita dalla gerarchia di composizione, consigliamo alcune best practice per verificare che gli oggetti si comportino in modo corretto nella piattaforma e si difendano dall'uso improprio. In particolare, utilizza i callback onRemembered (o onRetained) per avviare il lavoro anziché il costruttore, annulla tutto il lavoro quando gli oggetti non vengono più ricordati o conservati ed evita di divulgare implementazioni di RememberObserver e RetainObserver per evitare chiamate accidentali. La sezione successiva spiega questi consigli in modo più approfondito.

Inizializzazione e pulizia con RememberObserver e RetainObserver

La guida Thinking in Compose descrive il modello mentale alla base della composizione. Quando lavori con RememberObserver e RetainObserver, è importante tenere presente due comportamenti della composizione:

  • La ricomposizione è ottimistica e potrebbe essere annullata
  • Tutte le funzioni componibili non devono avere effetti collaterali

Esegui gli effetti collaterali dell'inizializzazione durante onRemembered o onRetained, non durante la costruzione

Quando gli oggetti vengono ricordati o conservati, la lambda di calcolo viene eseguita nell'ambito della composizione. Per gli stessi motivi per cui non esegui un effetto collaterale o avvii una coroutine durante la composizione, non devi eseguire effetti collaterali nella lambda di calcolo passata a remember, retain e alle relative varianti. Ciò include la parte del costruttore per gli oggetti memorizzati o conservati.

Quando implementi RememberObserver o RetainObserver, verifica che tutti gli effetti e i job avviati vengano inviati nel callback onRemembered. Offre la stessa tempistica delle API SideEffect. Garantisce inoltre che questi effetti vengano eseguiti solo quando viene applicata la composizione, il che impedisce lavori orfani e perdite di memoria se una ricomposizione viene abbandonata o posticipata.

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() { /* ... */ }

    // ...
}

Smontaggio in caso di dimenticanza, ritiro o abbandono

Per evitare perdite di risorse o l'abbandono di job in background, è necessario eliminare anche gli oggetti memorizzati. Per gli oggetti che implementano RememberObserver, ciò significa che qualsiasi elemento inizializzato in onRemembered deve avere una chiamata di rilascio corrispondente in onForgotten.

Poiché la composizione può essere annullata, gli oggetti che implementano RememberObserver devono anche pulire se vengono abbandonati nelle composizioni. Un oggetto viene abbandonato quando viene restituito da remember in una composizione che viene annullata o non riesce. (Ciò si verifica più comunemente quando si utilizza PausableComposition e può verificarsi anche quando si utilizza il ricaricamento rapido con gli strumenti di anteprima dei componenti componibili di Android Studio.)

Quando un oggetto memorizzato viene abbandonato, riceve solo la chiamata a onAbandoned (e nessuna chiamata a onRemembered). Per implementare il metodo di abbandono, elimina tutto ciò che è stato creato tra l'inizializzazione dell'oggetto e il momento in cui l'oggetto avrebbe ricevuto il callback 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()
    }
}

Mantenere private le implementazioni di RememberObserver e RetainObserver

Quando scrivi API pubbliche, fai attenzione quando estendi RememberObserver e RetainObserver nella creazione di classi restituite pubblicamente. Un utente potrebbe non ricordare il tuo oggetto quando ti aspetti che lo faccia o potrebbe ricordarlo in un modo diverso da quello che intendevi. Per questo motivo, ti consigliamo di non esporre costruttori o funzioni di fabbrica per oggetti che implementano RememberObserver o RetainObserver. Tieni presente che questo dipende dal tipo di runtime di una classe, non dal tipo dichiarato. Ricordare un oggetto che implementa RememberObserver o RetainObserver, ma viene eseguito il cast a Any, fa comunque sì che l'oggetto riceva callback.

Opzione non consigliata:

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()

Consigliato:

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
}

Considerazioni quando si memorizzano oggetti

Oltre ai consigli precedenti relativi a RememberObserver e RetainObserver, ti consigliamo anche di prestare attenzione ed evitare di ricordare accidentalmente gli oggetti sia per le prestazioni che per la correttezza. Le sezioni seguenti approfondiscono scenari specifici di riassegnazione e il motivo per cui devono essere evitati.

Ricorda gli oggetti una sola volta

Ricordare di nuovo un oggetto può essere pericoloso. Nel migliore dei casi, potresti sprecare risorse per ricordare un valore già memorizzato. Tuttavia, se un oggetto implementa RememberObserver e viene ricordato due volte inaspettatamente, riceverà più callback del previsto. Ciò può causare problemi, in quanto la logica di onRemembered e onForgotten verrà eseguita due volte e la maggior parte delle implementazioni di RememberObserver non supporta questo caso. Se una seconda chiamata remember viene eseguita in un ambito diverso con una durata diversa da remember originale, molte implementazioni di RememberObserver.onForgotten eliminano l'oggetto prima che il suo utilizzo sia terminato.

val first: RememberObserver = rememberFoo()

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

Questi suggerimenti non si applicano agli oggetti che vengono ricordati di nuovo in modo transitivo (ad esempio, gli oggetti ricordati che utilizzano un altro oggetto ricordato). È comune scrivere codice come segue, il che è consentito perché viene ricordato un oggetto diverso e quindi non causa un raddoppio imprevisto del callback.

val foo: Foo = rememberFoo()

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

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

Supponi che gli argomenti della funzione siano già memorizzati

Una funzione non deve memorizzare nessuno dei suoi parametri sia perché può portare a doppie chiamate di callback per RememberObserver sia perché è inutile. Se un parametro di input deve essere ricordato, verifica che non implementi RememberObserver o richiedi ai chiamanti di ricordare l'argomento.

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

Ciò non si applica agli oggetti ricordati in modo transitivo. Quando memorizzi un oggetto derivato dagli argomenti di una funzione, valuta la possibilità di specificarlo come una delle chiavi di remember:

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

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

Non conservare un oggetto già memorizzato

Come per il ricordo di un oggetto, dovresti evitare di conservare un oggetto ricordato per cercare di prolungarne la durata. Questo è il risultato del consiglio riportato in Durate degli stati: retain non deve essere utilizzato con oggetti che hanno una durata che non corrisponde a quella delle offerte di conservazione. Poiché gli oggetti remembered hanno una durata inferiore rispetto agli oggetti retained, non devi conservare un oggetto ricordato. Preferisci invece conservare l'oggetto nel sito di origine anziché memorizzarlo.