From c792ae5fdc8a4d62af49ea48c259d547eeb4ab19 Mon Sep 17 00:00:00 2001 From: Keith Abdulla Date: Tue, 19 Nov 2024 13:05:35 -0800 Subject: [PATCH] Refactor LifecycleOwner to ComposeLifecycleOwner for better lifecycle management - Extracted anonymous LifecycleOwner and RememberObserver implementation into a reusable ComposeLifecycleOwner class. - ComposeLifecycleOwner synchronizes its lifecycle with the parent Lifecycle and integrates with Compose's memory model via RememberObserver. - Improves code readability, reusability, and aligns lifecycle management with Compose best practices. --- .../ui/compose/ComposeLifecycleOwner.kt | 102 ++++++++++++++++++ .../workflow1/ui/compose/WorkflowRendering.kt | 44 -------- 2 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeLifecycleOwner.kt diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeLifecycleOwner.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeLifecycleOwner.kt new file mode 100644 index 000000000..8585269fd --- /dev/null +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeLifecycleOwner.kt @@ -0,0 +1,102 @@ +package com.squareup.workflow1.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Lifecycle.Event +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +/** + * Returns a [LifecycleOwner] that is a mirror of the current [LocalLifecycleOwner] until this + * function leaves the composition. Similar to [WorkflowLifecycleOwner] for views, but a + * bit simpler since we don't need to worry about attachment state. + */ +@Composable internal fun rememberChildLifecycleOwner(): LifecycleOwner { + val parentLifecycle = LocalLifecycleOwner.current.lifecycle + val lifecycleOwner = remember(parentLifecycle) { + ComposeLifecycleOwner.installOn(parentLifecycle) + } + return lifecycleOwner +} + +/** + * A custom [LifecycleOwner] that synchronizes its lifecycle with a parent [Lifecycle] and + * integrates with Jetpack Compose's lifecycle through [RememberObserver]. + * + * ## Purpose + * + * - Ensures that any lifecycle-aware components within a composable function have a lifecycle that + * accurately reflects both the parent lifecycle and the composable's own lifecycle. + * - Manages lifecycle transitions and observer registration/removal to prevent memory leaks and + * ensure proper cleanup when the composable leaves the composition. + * + * ## Key Features + * + * - Lifecycle Synchronization: Mirrors lifecycle events from the provided `parentLifecycle` to + * its own [LifecycleRegistry], ensuring consistent state transitions. + * - Compose Integration: Implements [RememberObserver] to align with the composable's lifecycle + * in the Compose memory model. + * - Automatic Observer Management: Adds and removes a [LifecycleEventObserver] to the parent + * lifecycle, preventing leaks and ensuring proper disposal. + * - **State Transition Safety:** Carefully manages lifecycle state changes to avoid illegal + * transitions, especially during destruction. + * + * ## Usage Notes + * + * - Should be used in conjunction with `remember` and provided the `parentLifecycle` as a key to + * ensure it updates correctly when the parent lifecycle changes. + * - By integrating with Compose's lifecycle, it ensures that resources are properly released when + * the composable leaves the composition. + * + * @param parentLifecycle The parent [Lifecycle] with which this lifecycle owner should synchronize. + */ +private class ComposeLifecycleOwner( + private val parentLifecycle: Lifecycle +) : LifecycleOwner, RememberObserver, LifecycleEventObserver { + + private val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle + get() = registry + + override fun onRemembered() { + } + + override fun onAbandoned() { + onForgotten() + } + + override fun onForgotten() { + parentLifecycle.removeObserver(this) + + // If we're leaving the composition, ensure the lifecycle is cleaned up + if (registry.currentState != Lifecycle.State.INITIALIZED) { + registry.currentState = Lifecycle.State.DESTROYED + } + } + + override fun onStateChanged( + source: LifecycleOwner, + event: Event + ) { + registry.handleLifecycleEvent(event) + } + + companion object { + fun installOn(parentLifecycle: Lifecycle): ComposeLifecycleOwner { + return ComposeLifecycleOwner(parentLifecycle).also { + // We need to synchronize the lifecycles before the child ever even sees the lifecycle + // because composes contract tries to guarantee that the lifecycle is in at least the + // CREATED state by the time composition is actually running. If we don't synchronize + // the lifecycles right away, then we break that invariant. One concrete case of this is + // that SavedStateRegistry requires its lifecycle to be CREATED before reading values + // from it, and consuming values from an SSR is a valid thing to do from composition + // directly, and in fact AndroidComposeView itself does this. + parentLifecycle.addObserver(it) + } + } + } +} diff --git a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt index 8d2552e2b..7801a39cb 100644 --- a/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt +++ b/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/WorkflowRendering.kt @@ -3,17 +3,11 @@ package com.squareup.workflow1.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Lifecycle.State.DESTROYED -import androidx.lifecycle.Lifecycle.State.INITIALIZED -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ScreenViewHolder @@ -94,41 +88,3 @@ public fun WorkflowRendering( } } } - -/** - * Returns a [LifecycleOwner] that is a mirror of the current [LocalLifecycleOwner] until this - * function leaves the composition. Similar to [WorkflowLifecycleOwner] for views, but a - * bit simpler since we don't need to worry about attachment state. - */ -@Composable private fun rememberChildLifecycleOwner(): LifecycleOwner { - val lifecycleOwner = remember { - object : LifecycleOwner { - val registry = LifecycleRegistry(this) - override val lifecycle: Lifecycle - get() = registry - } - } - val parentLifecycle = LocalLifecycleOwner.current.lifecycle - - DisposableEffect(parentLifecycle) { - val parentObserver = LifecycleEventObserver { _, event -> - // Any time the parent lifecycle changes state, perform the same change on our lifecycle. - lifecycleOwner.registry.handleLifecycleEvent(event) - } - - parentLifecycle.addObserver(parentObserver) - onDispose { - parentLifecycle.removeObserver(parentObserver) - - // If we're leaving the composition it means the WorkflowRendering is either going away itself - // or about to switch to an incompatible rendering – either way, this lifecycle is dead. Note - // that we can't transition from INITIALIZED to DESTROYED – the LifecycleRegistry will throw. - // WorkflowLifecycleOwner has this same check. - if (lifecycleOwner.registry.currentState != INITIALIZED) { - lifecycleOwner.registry.currentState = DESTROYED - } - } - } - - return lifecycleOwner -}