Compor callbacks de estado com RememberObserver e RetainObserver

No Jetpack Compose, um objeto pode implementar RememberObserver para receber callbacks quando usado com remember para saber quando ele começa e para de ser lembrado na hierarquia de composição. Da mesma forma, você pode usar RetainObserver para receber informações sobre o estado de um objeto usado com retain.

Para objetos que usam essas informações de ciclo de vida da hierarquia de composição, recomendamos algumas práticas recomendadas para verificar se os objetos agem como bons cidadãos na plataforma e se defendem contra o uso indevido. Especificamente, use os callbacks onRemembered (ou onRetained) para iniciar o trabalho em vez do construtor, cancele todo o trabalho quando os objetos deixarem de ser lembrados ou retidos e evite vazar implementações de RememberObserver e RetainObserver para evitar chamadas acidentais. A próxima seção explica essas recomendações em mais detalhes.

Inicialização e limpeza com RememberObserver e RetainObserver

O guia "Pensando em Compose" descreve o modelo mental por trás da composição. Ao trabalhar com RememberObserver e RetainObserver, é importante ter em mente dois comportamentos de composição:

  • A recomposição é otimista e pode ser cancelada
  • Todas as funções combináveis não podem ter efeitos colaterais

Execute efeitos colaterais de inicialização durante onRemembered ou onRetained, não durante a construção.

Quando os objetos são lembrados ou mantidos, a lambda de cálculo é executada como parte da composição. Pelos mesmos motivos que você não realizaria um efeito colateral ou iniciaria uma corrotina durante a composição, também não é recomendável realizar efeitos colaterais no lambda de cálculo transmitido para remember, retain e suas variações. Isso inclui como parte do construtor dos objetos lembrados ou retidos.

Em vez disso, ao implementar RememberObserver ou RetainObserver, verifique se todos os efeitos e jobs iniciados são enviados no callback onRemembered. Isso oferece o mesmo tempo das APIs SideEffect. Isso também garante que esses efeitos só sejam executados quando a composição é aplicada, o que evita jobs órfãos e vazamentos de memória se uma recomposição for abandonada ou adiada.

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

    // ...
}

Desmontagem quando esquecida, removida ou abandonada

Para evitar vazamento de recursos ou deixar jobs em segundo plano órfãos, os objetos lembrados também precisam ser descartados. Para objetos que implementam RememberObserver, isso significa que tudo o que for inicializado em onRemembered precisa ter uma chamada de liberação correspondente em onForgotten.

Como a composição pode ser cancelada, os objetos que implementam RememberObserver também precisam se organizar se forem abandonados em composições. Um objeto é abandonado quando é retornado por remember em uma composição que é cancelada ou falha. Isso acontece com mais frequência ao usar PausableComposition e também pode ocorrer ao usar a recarga dinâmica com as ferramentas de prévia combináveis do Android Studio.

Quando um objeto lembrado é abandonado, ele recebe apenas a chamada para onAbandoned (e não para onRemembered). Para implementar o método de abandono, descarte tudo o que foi criado entre a inicialização do objeto e o momento em que o objeto teria recebido o 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()
    }
}

Mantenha as implementações de RememberObserver e RetainObserver privadas

Ao escrever APIs públicas, tenha cuidado ao estender RememberObserver e RetainObserver na criação de classes que são retornadas publicamente. Um usuário pode não se lembrar do seu objeto quando você espera que ele se lembre ou pode se lembrar de uma maneira diferente da que você pretendia. Por isso, recomendamos não expor construtores ou funções de fábrica para objetos que implementam RememberObserver ou RetainObserver. Isso depende do tipo de tempo de execução de uma classe, não do tipo declarado. Por exemplo, lembrar um objeto que implementa RememberObserver ou RetainObserver, mas é convertido para Any, ainda faz com que o objeto receba callbacks.

Não recomendado:

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

Recomendado:

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
}

Considerações ao lembrar objetos

Além das recomendações anteriores sobre RememberObserver e RetainObserver, também recomendamos evitar a recriação acidental de objetos, tanto para performance quanto para correção. As seções a seguir abordam com mais detalhes cenários específicos de relembrança e por que eles devem ser evitados.

Lembrar objetos apenas uma vez

Relembrar um objeto pode ser perigoso. No melhor dos casos, você pode estar desperdiçando recursos ao lembrar um valor que já foi lembrado. Mas se um objeto implementar RememberObserver e for lembrado duas vezes inesperadamente, ele receberá mais callbacks do que o esperado. Isso pode causar problemas, já que a lógica de onRemembered e onForgotten será executada duas vezes, e a maioria das implementações de RememberObserver não aceita esse caso. Se uma segunda chamada de lembrança acontecer em um escopo diferente com uma duração diferente do remember original, muitas implementações de RememberObserver.onForgotten descartarão o objeto antes que ele termine de ser usado.

val first: RememberObserver = rememberFoo()

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

Essa dica não se aplica a objetos que são lembrados novamente de forma transitiva (ou seja, objetos lembrados que consomem outro objeto lembrado). É comum escrever um código como este, que é permitido porque um objeto diferente está sendo lembrado e, portanto, não causa duplicação inesperada de callback.

val foo: Foo = rememberFoo()

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

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

Suponha que os argumentos da função já foram lembrados

Uma função não deve se lembrar de nenhum dos parâmetros dela, porque isso pode levar a invocações de callback duplas para RememberObserver e porque é desnecessário. Se um parâmetro de entrada precisar ser lembrado, verifique se ele não implementa RememberObserver ou exija que os chamadores se lembrem do argumento.

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

Isso não se aplica a objetos lembrados de forma transitiva. Ao lembrar um objeto derivado dos argumentos de uma função, especifique-o como uma das chaves de remember:

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

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

Não reter um objeto que já foi lembrado

Assim como ao lembrar de um objeto, evite reter um objeto que foi lembrado para tentar estender a vida útil dele. Isso é uma consequência da recomendação em Ciclos de vida do estado: retain não deve ser usado com objetos que têm um ciclo de vida que não corresponde ao ciclo de vida das ofertas de retenção. Como os objetos remembered têm um ciclo de vida mais curto do que os objetos retained, não retenha um objeto lembrado. Em vez disso, prefira manter o objeto no site de origem em vez de se lembrar dele.