Call onRelease callback in the same order as onForgotten
SubcomposeLayout depends on this order to dispose nested subcompositions when parent exits the composition. In particular, we want to make sure that child can unpin itself (and cleanup e.g. focus) before parent onForgotten is dispatched. Deactivations were already dispatched in this order to account for a similar issue.
Fixes: 322579799
Test: Added ordering test in SubcomposeLayoutTest and pinning test in LazyListPinnableContainerTest
(cherry picked from https://android-review.googlesource.com/q/commit:01c9d9953b00e5111ff8a2075208ff78601a572e)
(cherry picked from https://android-review.googlesource.com/q/commit:13a8f4633327392402d5282a254eabea94f8f526)
Merged-In: I0429d56ae6d02406fbc6f062d1c1ca040030ab09
Change-Id: I0429d56ae6d02406fbc6f062d1c1ca040030ab09
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
index 82963da..30c436c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -666,6 +667,42 @@
assertThat(parent2Pinned).isTrue()
}
}
+
+ @Test
+ fun pinnedItemIsRemovedAfterContainerExitsComposition() {
+ var active by mutableStateOf(true)
+ // Arrange.
+ rule.setContentParameterized {
+ if (active) {
+ LazyColumn(Modifier.size(itemSize * 2)) {
+ items(3) { colIndex ->
+ LazyRow {
+ items(3) { rowIndex ->
+ if (colIndex == 1 && rowIndex == 1) {
+ pinnableContainer = LocalPinnableContainer.current
+ }
+ Box(Modifier.size(itemSize).testTag("$colIndex:$rowIndex"))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1:1")
+ .assertIsPlaced()
+
+ rule.runOnIdle {
+ requireNotNull(pinnableContainer).pin()
+ }
+
+ rule.runOnIdle {
+ active = !active
+ }
+
+ rule.onNodeWithTag("1:1")
+ .assertIsNotPlaced()
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 83eaa14..7810951 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -17,6 +17,8 @@
@file:OptIn(InternalComposeApi::class)
package androidx.compose.runtime
+import androidx.collection.MutableScatterSet
+import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.changelist.ChangeList
import androidx.compose.runtime.collection.IdentityArrayMap
import androidx.compose.runtime.collection.IdentityArraySet
@@ -1236,7 +1238,7 @@
private val remembering = mutableListOf<RememberObserver>()
private val forgetting = mutableListOf<Any>()
private val sideEffects = mutableListOf<() -> Unit>()
- private var releasing: MutableList<ComposeNodeLifecycleCallback>? = null
+ private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
override fun remembering(instance: RememberObserver) {
remembering.add(instance)
@@ -1255,15 +1257,18 @@
}
override fun releasing(instance: ComposeNodeLifecycleCallback) {
- (releasing ?: mutableListOf<ComposeNodeLifecycleCallback>().also {
- releasing = it
- }) += instance
+ val releasing = releasing
+ ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
+
+ releasing += instance
+ forgetting += instance
}
fun dispatchRememberObservers() {
// Send forgets and node deactivations
if (forgetting.isNotEmpty()) {
trace("Compose:onForgotten") {
+ val releasing = releasing
for (i in forgetting.size - 1 downTo 0) {
val instance = forgetting[i]
abandoning.remove(instance)
@@ -1271,7 +1276,12 @@
instance.onForgotten()
}
if (instance is ComposeNodeLifecycleCallback) {
- instance.onDeactivate()
+ // node callbacks are in the same queue as forgets to ensure ordering
+ if (releasing != null && instance in releasing) {
+ instance.onRelease()
+ } else {
+ instance.onDeactivate()
+ }
}
}
}
@@ -1286,17 +1296,6 @@
}
}
}
-
- // Send node releases
- val releasing = releasing
- if (!releasing.isNullOrEmpty()) {
- trace("Compose:releases") {
- for (i in releasing.size - 1 downTo 0) {
- val instance = releasing[i]
- instance.onRelease()
- }
- }
- }
}
fun dispatchSideEffects() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index e3bd0f6..ef2de8e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.composed
import androidx.compose.ui.draw.assertColor
import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.focus.isExactly
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
@@ -2694,6 +2695,47 @@
}
}
+ @Test
+ fun nestedDisposeIsCalledInOrder() {
+ val disposeOrder = mutableListOf<String>()
+ var active by mutableStateOf(true)
+ rule.setContent {
+ if (active) {
+ BoxWithConstraints {
+ BoxWithConstraints {
+ DisposableEffect(Unit) {
+ onDispose {
+ disposeOrder += "inner 1"
+ }
+ }
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ disposeOrder += "outer"
+ }
+ }
+
+ BoxWithConstraints {
+ DisposableEffect(Unit) {
+ onDispose {
+ disposeOrder += "inner 2"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ active = false
+ }
+
+ rule.runOnIdle {
+ assertThat(disposeOrder).isExactly("inner 2", "outer", "inner 1")
+ }
+ }
+
private fun SubcomposeMeasureScope.measure(
slotId: Any,
constraints: Constraints,