使用 RememberObserver 和 RetainObserver 组合状态回调

在 Jetpack Compose 中,对象可以实现 RememberObserver,以便在与 remember 搭配使用时接收回调,从而了解该对象何时开始和停止在组合层次结构中被记住。同样,您可以使用 RetainObserver 接收有关与 retain 一起使用的对象的状态的信息。

对于使用来自组合层次结构的此生命周期信息的对象,我们建议采用一些最佳实践来验证您的对象是否在平台中表现良好并防范滥用行为。具体来说,请使用 onRemembered(或 onRetained)回调来启动工作,而不是使用构造函数;当对象不再被记住或保留时,取消所有工作;避免泄露 RememberObserverRetainObserver 的实现,以免发生意外调用。下一部分将更深入地介绍这些建议。

使用 RememberObserverRetainObserver 进行初始化和清理

“Compose 思维”指南介绍了组合背后的思维模式。使用 RememberObserverRetainObserver 时,请务必注意以下两种组合行为:

  • 重组是乐观的操作,可能会被取消
  • 所有可组合函数都不应有附带效应

onRememberedonRetained 期间运行初始化副作用,而不是在构造期间运行

当对象被记忆或保留时,计算 lambda 会作为组合的一部分运行。出于与在组合期间不执行附带效应或启动协程相同的原因,您也不应在传递给 rememberretain 及其变体的计算 lambda 中执行附带效应。这包括作为记忆对象或保留对象的构造函数的一部分。

因此,在实现 RememberObserverRetainObserver 时,请验证所有 effect 和启动的作业是否都在 onRemembered 回调中分派。这与 SideEffect API 的时间安排相同。它还保证这些效应仅在应用组合时执行,从而防止在放弃或延迟重新组合时出现孤立作业和内存泄漏。

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

    // ...
}

在忘记、弃用或废弃时拆解

为避免泄漏资源或使后台作业成为孤立作业,还必须处置已记住的对象。对于实现 RememberObserver 的对象,这意味着在 onRemembered 中初始化的任何内容都必须在 onForgotten 中有相应的释放调用。

由于组合可以取消,因此实现 RememberObserver 的对象如果被放弃在组合中,也必须自行清理。当对象在被取消或失败的组合中由 remember 返回时,该对象会被舍弃。(这种情况最常在使用 PausableComposition 时发生,在使用 Android Studio 的可组合项预览工具进行热重载时也可能会发生。)

当记忆的对象被舍弃时,它只会收到对 onAbandoned 的调用(而不会收到对 onRemembered 的调用)。如需实现舍弃方法,请处置在对象初始化时与对象本应收到 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()
    }
}

使 RememberObserverRetainObserver 实现保持私密状态

在编写公共 API 时,请谨慎扩展 RememberObserverRetainObserver 以创建公开返回的类。用户可能在您希望他们记住您的对象时没有记住,或者以与您预期不同的方式记住了您的对象。因此,我们建议不要为实现 RememberObserverRetainObserver 的对象公开构造函数或工厂函数。请注意,这取决于类的运行时类型,而不是声明的类型 - 记住实现 RememberObserverRetainObserver 但被强制转换为 Any 的对象仍会导致该对象接收回调。

不推荐:

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

推荐:

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
}

记忆对象时的注意事项

除了之前有关 RememberObserverRetainObserver 的建议之外,我们还建议您注意并避免因性能和正确性问题而意外重新记忆对象。以下各部分将更深入地探讨具体的重新记忆场景以及为何应避免这些场景。

仅记住对象一次

重新记住对象可能很危险。在最佳情况下,您可能在浪费资源来记住已记住的值。但如果某个对象实现了 RememberObserver 并且意外地被记忆了两次,那么它将收到比预期更多的回调。这可能会导致问题,因为 onRememberedonForgotten 逻辑将执行两次,而大多数 RememberObserver 实现不支持这种情况。如果在与原始 remember 具有不同生命周期的不同范围内发生第二次 remember 调用,许多 RememberObserver.onForgotten 实现会在对象完成使用之前将其处置。

val first: RememberObserver = rememberFoo()

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

此建议不适用于以传递方式再次记住的对象(即,消耗另一个记住的对象的记住的对象)。通常会编写如下所示的代码,这是允许的,因为系统会记住不同的对象,因此不会导致意外的回调加倍。

val foo: Foo = rememberFoo()

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

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

假设函数实参已被记住

函数不应记住任何参数,因为这可能会导致 RememberObserver 的双重回调调用,而且也没有必要。如果必须记住某个输入参数,请验证该参数是否未实现 RememberObserver,或者要求调用方记住其实参。

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

这不适用于以传递方式记住的对象。在记住从函数实参派生的对象时,请考虑将其指定为 remember 的键之一:

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

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

不保留已记住的对象

与重新记忆对象类似,您应避免保留已记忆的对象,以尝试延长其使用期限。这是 State lifespans(状态生命周期)中的建议带来的影响:retain 不应与生命周期与保留提供的生命周期不匹配的对象一起使用。由于 remembered 对象的生命周期比 retained 对象短,因此不应保留记忆的对象。相反,最好在来源网站上保留对象,而不是记住它。