Compose state callbacks with RememberObserver and RetainObserver

In Jetpack Compose, an object can implement RememberObserver to receive callbacks when it's used with remember to know when it starts and stops being remembered in the composition hierarchy. Similarly, you can use RetainObserver to receive information about the state of an object used with retain.

For objects that use this lifecycle information from the composition hierarchy, we recommend a few best practices to verify that your objects act as good citizens in the platform and defend against misuse. Specifically, use the onRemembered (or onRetained) callbacks to launch work instead of the constructor, cancel all work when objects stop being remembered or retained, and avoid leaking implementations of RememberObserver and RetainObserver to avoid accidental calls. The next section explains these recommendations in more depth.

Initialization and cleanup with RememberObserver and RetainObserver

The Thinking in Compose guide describes the mental model behind composition. When working with RememberObserver and RetainObserver, it's important to keep in mind two behaviors of composition:

  • Recomposition is optimistic and may be canceled
  • All composable functions should have no side-effects

Run initialization side effects during onRemembered or onRetained, not construction

When objects are remembered or retained, the calculation lambda runs as part of composition. For the same reasons that you wouldn't perform a side-effect or launch a coroutine during composition, you should also not perform side-effects in the calculation lambda passed to remember, retain, and their variations. This includes as part of the constructor for the remembered or retained objects.

Instead, when implementing RememberObserver, or RetainObserver, verify that all effects and launched jobs are dispatched in the onRemembered callback. This offers the same timing as the SideEffect APIs. It also guarantees that these effects only execute when the composition is applied, which prevents orphaned jobs and memory leaks if a recomposition is abandoned or deferred.

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

    // ...
}

Teardown when forgotten, retired, or abandoned

To avoid leaking resources or leaving background jobs orphaned, remembered objects must also be disposed of. For objects that implement RememberObserver, this means that anything initialized in onRemembered must have a matching release call in onForgotten.

Because composition can be canceled, objects that implement RememberObserver must also tidy up after themselves if they are abandoned in compositions. An object is abandoned when it is returned by remember in a composition that gets canceled or fails. (This happens most commonly when using PausableComposition, and can also occur when using hot reload with Android Studio's composable preview tooling.)

When a remembered object is abandoned, it receives only the call to onAbandoned (and no call to onRemembered). To implement the abandon method, dispose of anything created between when the object is initialized and when the object would have received the onRemembered callback.

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

Keep RememberObserver and RetainObserver implementations private

When writing public APIs, use caution when extending RememberObserver and RetainObserver in creating classes that are returned publicly. A user may not remember your object when you expect them to, or may remember your object in a different way than you intended. Because of this, we recommend not exposing constructors or factory functions for objects that implement RememberObserver or RetainObserver. Note that this is dependent on the runtime type of a class, not the declared type — remembering an object that implements RememberObserver or RetainObserver but is casted to Any still causes the object to receive callbacks.

Not recommended:

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

Recommended:

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
}

Considerations when remembering objects

In addition to the previous recommendations surrounding RememberObserver and RetainObserver, we also recommend being mindful of and avoiding accidentally re-remembering objects both for performance and correctness. The following sections go into more depth on specific re-remembering scenarios and why they should be avoided.

Only remember objects once

Re-remembering an object can be dangerous. In the best case scenario, you may be wasting resources remembering a value that's already remembered. But if an object implements RememberObserver and is remembered twice unexpectedly, it will receive more callbacks than it expects. This can cause issues, as the onRemembered and onForgotten logic will execute twice, and most implementations of RememberObserver don't support this case. If a second remember call happens in a different scope that has a different lifespan from the original remember, many implementations of RememberObserver.onForgotten dispose the object before it is finished being used.

val first: RememberObserver = rememberFoo()

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

This advice does not apply to objects that are remembered again transitively (as in, remembered objects that consume another remembered object). It is common to write code that looks as follows, which is allowable because a different object is being remembered and therefore does not cause unexpected callback doubling.

val foo: Foo = rememberFoo()

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

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

Assume function arguments are already remembered

A function shouldn't remember any of its parameters both because it can lead to double callback invocations for RememberObserver and because it is unnecessary. If an input parameter must be remembered, either verify that it does not implement RememberObserver, or require callers to remember their argument.

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

This does not apply to objects remembered transitively. When remembering an object derived from a function's arguments, consider specifying it as one of the keys to remember:

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

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

Don't retain an object that's already remembered

Similar to re-remembering an object, you should avoid retaining an object that's remembered to try and extend its lifespan. This is a fallout of the advice in State lifespans: retain shouldn't be used with objects that have a lifespan that doesn't match the lifespan retain offers. Since remembered objects have a shorter lifespan than retained objects, you shouldn't retain a remembered object. Instead, prefer retaining the object at the origin site instead of remembering it.