Cómo componer devoluciones de llamadas de estado con RememberObserver y RetainObserver

En Jetpack Compose, un objeto puede implementar RememberObserver para recibir devoluciones de llamada cuando se usa con remember y saber cuándo comienza y deja de recordarse en la jerarquía de composición. Del mismo modo, puedes usar RetainObserver para recibir información sobre el estado de un objeto que se usa con retain.

En el caso de los objetos que usan esta información del ciclo de vida de la jerarquía de composición, te recomendamos algunas prácticas recomendadas para verificar que tus objetos actúen como buenos ciudadanos en la plataforma y se defiendan contra el uso inadecuado. Específicamente, usa las devoluciones de llamada onRemembered (o onRetained) para iniciar el trabajo en lugar del constructor, cancela todo el trabajo cuando los objetos dejan de recordarse o retenerse, y evita filtrar implementaciones de RememberObserver y RetainObserver para evitar llamadas accidentales. En la siguiente sección, se explican estas recomendaciones con más detalle.

Inicialización y limpieza con RememberObserver y RetainObserver

La guía de Thinking in Compose describe el modelo mental detrás de la composición. Cuando trabajes con RememberObserver y RetainObserver, es importante que tengas en cuenta dos comportamientos de composición:

  • La recomposición es optimista y se puede cancelar
  • Todas las funciones de componibilidad no deben tener efectos secundarios.

Ejecuta efectos secundarios de inicialización durante onRemembered o onRetained, no durante la construcción

Cuando se recuerdan o retienen objetos, la lambda de cálculo se ejecuta como parte de la composición. Por los mismos motivos por los que no realizarías un efecto secundario ni iniciarías una corrutina durante la composición, tampoco deberías realizar efectos secundarios en la expresión lambda de cálculo que se pasa a remember, retain y sus variaciones. Esto se incluye como parte del constructor de los objetos recordados o retenidos.

En cambio, cuando implementes RememberObserver o RetainObserver, verifica que todos los efectos y los trabajos iniciados se despachen en la devolución de llamada onRemembered. Esto ofrece la misma sincronización que las APIs de SideEffect. También garantiza que estos efectos solo se ejecuten cuando se aplique la composición, lo que evita trabajos huérfanos y pérdidas de memoria si se abandona o se aplaza una recomposición.

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

    // ...
}

Desmontaje cuando se olvida, se retira o se abandona

Para evitar la pérdida de recursos o que los trabajos en segundo plano queden huérfanos, también se deben descartar los objetos recordados. Para los objetos que implementan RememberObserver, esto significa que todo lo que se inicializa en onRemembered debe tener una llamada de liberación coincidente en onForgotten.

Dado que la composición se puede cancelar, los objetos que implementan RememberObserver también deben limpiarse si se abandonan en las composiciones. Un objeto se abandona cuando remember lo devuelve en una composición que se cancela o falla. (Esto sucede con mayor frecuencia cuando se usa PausableComposition y también puede ocurrir cuando se usa la recarga en caliente con las herramientas de vista previa de elementos componibles de Android Studio).

Cuando se abandona un objeto recordado, solo recibe la llamada a onAbandoned (y no la llamada a onRemembered). Para implementar el método de abandono, desecha todo lo que se haya creado entre el momento en que se inicializa el objeto y el momento en que el objeto habría recibido la devolución de llamada 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()
    }
}

Mantén privadas las implementaciones de RememberObserver y RetainObserver

Cuando escribas APIs públicas, ten cuidado al extender RememberObserver y RetainObserver para crear clases que se devuelvan de forma pública. Es posible que un usuario no recuerde tu objeto cuando esperas que lo haga, o que lo recuerde de una manera diferente a la que esperabas. Por este motivo, recomendamos no exponer constructores ni funciones de fábrica para objetos que implementen RememberObserver o RetainObserver. Ten en cuenta que esto depende del tipo de tiempo de ejecución de una clase, no del tipo declarado. Recordar un objeto que implementa RememberObserver o RetainObserver, pero que se convierte en Any, aún hace que el objeto reciba devoluciones de llamada.

No se recomienda:

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
}

Consideraciones para recordar objetos

Además de las recomendaciones anteriores sobre RememberObserver y RetainObserver, también recomendamos tener en cuenta y evitar volver a recordar objetos de forma accidental, tanto por rendimiento como por corrección. En las siguientes secciones, se profundiza en situaciones específicas de re-recordatorio y por qué se deben evitar.

Solo recuerda los objetos una vez

Volver a recordar un objeto puede ser peligroso. En el mejor de los casos, es posible que desperdicies recursos recordando un valor que ya se recordó. Sin embargo, si un objeto implementa RememberObserver y se recuerda dos veces de forma inesperada, recibirá más devoluciones de llamada de las que espera. Esto puede causar problemas, ya que la lógica de onRemembered y onForgotten se ejecutará dos veces, y la mayoría de las implementaciones de RememberObserver no admiten este caso. Si se realiza una segunda llamada de recordar en un alcance diferente que tiene una vida útil diferente de la remember original, muchas implementaciones de RememberObserver.onForgotten descartan el objeto antes de que se termine de usar.

val first: RememberObserver = rememberFoo()

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

Esta sugerencia no se aplica a los objetos que se recuerdan de forma transitiva (es decir, los objetos recordados que consumen otro objeto recordado). Es común escribir código que se ve de la siguiente manera, lo que se permite porque se recuerda un objeto diferente y, por lo tanto, no causa una duplicación inesperada de la devolución de llamada.

val foo: Foo = rememberFoo()

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

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

Supón que los argumentos de la función ya se recuerdan

Una función no debe recordar ninguno de sus parámetros, ya que puede generar invocaciones de devolución de llamada dobles para RememberObserver y porque es innecesario. Si se debe recordar un parámetro de entrada, verifica que no implemente RememberObserver o exige que los llamadores recuerden su argumento.

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

Esto no se aplica a los objetos recordados de forma transitiva. Cuando recuerdes un objeto derivado de los argumentos de una función, considera especificarlo como una de las claves de remember:

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

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

No retener un objeto que ya se recuerda

Al igual que cuando se vuelve a recordar un objeto, debes evitar retener un objeto que se recuerda para intentar extender su vida útil. Esto es una consecuencia del consejo que se brinda en Duración del estado: retain no se debe usar con objetos que tengan una duración que no coincida con la duración de las ofertas de retención. Dado que los objetos remembered tienen una vida útil más corta que los objetos retained, no debes conservar un objeto recordado. En cambio, prefiere retener el objeto en el sitio de origen en lugar de recordarlo.