Clean up when seekable transition is disposed
This CL ensures the clean-up on dispose also covers Seekable
transition case. As a part of the clean-up, the scope from
the (disposed) transition is removed from the SnapshotStateObserver,
which outlives the transition. This change effectively avoids
leaking transition state, as well as any user state that is used
as target states, such as context.
Bug: 336167334
Test: new test added
(cherry picked from https://android-review.googlesource.com/q/commit:55e0d5b0e06d9aca7023aae6c63dfb9787e39546)
(cherry picked from https://android-review.googlesource.com/q/commit:a81139acf6a05200fa7f10df831e4b562bfcf113)
Merged-In: I89953ff6338e468ef88dfc06e0cb02e88ef8405a
Change-Id: I89953ff6338e468ef88dfc06e0cb02e88ef8405a
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
index 08fe481..f5575e6 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
@@ -39,6 +39,7 @@
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
@@ -2481,6 +2482,35 @@
}
}
+ @Test
+ fun testCleanupAfterDispose() {
+ fun isObserving(): Boolean {
+ var active = false
+ SeekableStateObserver.clearIf {
+ active = true
+ false
+ }
+ return active
+ }
+
+ var seekableState: SeekableTransitionState<*>?
+ var disposed by mutableStateOf(false)
+
+ rule.setContent {
+ seekableState = remember { SeekableTransitionState(true) }
+
+ if (!disposed) {
+ rememberTransition(transitionState = seekableState!!)
+ }
+ }
+ rule.waitForIdle()
+ assertTrue(isObserving())
+
+ disposed = true
+ rule.waitForIdle()
+ assertFalse(isObserving())
+ }
+
@OptIn(ExperimentalTransitionApi::class)
@Test
fun quickAddAndRemove() {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index aaa3d19..ef346a1 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -207,11 +207,9 @@
it.onTotalDurationChanged()
}
-private val SeekableStateObserver: SnapshotStateObserver by lazy(LazyThreadSafetyMode.NONE) {
- SnapshotStateObserver { it() }.apply {
- start()
- }
-}
+// This observer is also accessed from test. It should be otherwise treated as private.
+internal val SeekableStateObserver: SnapshotStateObserver by
+ lazy(LazyThreadSafetyMode.NONE) { SnapshotStateObserver { it() }.apply { start() } }
/**
* A [TransitionState] that can manipulate the progress of the [Transition] by seeking
@@ -833,12 +831,12 @@
}
} else {
transition.animateTo(transitionState.targetState)
- DisposableEffect(transition) {
- onDispose {
- // Clean up on the way out, to ensure the observers are not stuck in an in-between
- // state.
- transition.onDisposed()
- }
+ }
+ DisposableEffect(transition) {
+ onDispose {
+ // Clean up on the way out, to ensure the observers are not stuck in an in-between
+ // state.
+ transition.onDisposed()
}
}
return transition