Merge "Fix immediate invalidations for moved movable content" into androidx-main
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt
new file mode 100644
index 0000000..c17130f
--- /dev/null
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidMovableContentTests.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AndroidMovableContentTests : BaseComposeTest() {
+ @get:Rule
+ override val activityRule = makeTestActivityRule()
+
+ @Test
+ fun testMovableContentParameterInBoxWithConstraints() {
+ var state by mutableStateOf(false)
+ var lastSeen: Boolean? = null
+ val content = movableContentOf { parameter: Boolean ->
+ val content = @Composable {
+ lastSeen = parameter
+ }
+ Container(content)
+ }
+
+ // Infrastructure to avoid throwing a failure on the UI thread.
+ val failureReasons = mutableListOf<String>()
+ fun failed(reason: String) {
+ failureReasons.add(reason)
+ }
+
+ fun phase(description: String): Phase {
+ return object : Phase {
+ override fun expect(actual: Any?): Result {
+ return object : Result {
+ override fun toEqual(expected: Any?) {
+ if (expected != actual) {
+ failed("$description, expected $actual to be $expected")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ compose {
+ if (state) {
+ content(true)
+ } else {
+ BoxWithConstraints {
+ content(false)
+ }
+ }
+ }.then {
+ phase("In initial composition").expect(lastSeen).toEqual(state)
+ state = true
+ }.then {
+ phase("When setting state to true").expect(lastSeen).toEqual(state)
+ state = false
+ }.then {
+ phase("When setting state to false").expect(lastSeen).toEqual(state)
+ }.done()
+
+ assertEquals("", failureReasons.joinToString())
+ }
+}
+
+@Composable
+fun Container(content: @Composable () -> Unit) {
+ content()
+}
+
+interface Phase {
+ fun expect(actual: Any?): Result
+}
+
+interface Result {
+ fun toEqual(expected: Any?)
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index af9f3bc..3c0412e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -2674,6 +2674,7 @@
internal fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean {
val anchor = scope.anchor ?: return false
+ val slotTable = reader.table
val location = anchor.toIndexFor(slotTable)
if (isComposing && location >= reader.currentGroup) {
// if we are invalidating a scope that is going to be traversed during this
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 165ab21..83dc817e 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
@@ -933,15 +933,24 @@
scope.defaultsInvalid = true
}
val anchor = scope.anchor
- if (anchor == null || !slotTable.ownsAnchor(anchor) || !anchor.valid)
- return InvalidationResult.IGNORED // The scope has not yet entered the composition
- if (!anchor.valid)
+ if (anchor == null || !anchor.valid)
return InvalidationResult.IGNORED // The scope was removed from the composition
+ if (!slotTable.ownsAnchor(anchor)) {
+ // The scope might be owned by the delegate
+ val delegate = synchronized(lock) { invalidationDelegate }
+ if (delegate?.tryImminentInvalidation(scope, instance) == true)
+ return InvalidationResult.IMMINENT // The scope was owned by the delegate
+
+ return InvalidationResult.IGNORED // The scope has not yet entered the composition
+ }
if (!scope.canRecompose)
return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated
return invalidateChecked(scope, anchor, instance)
}
+ private fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean =
+ isComposing && composer.tryImminentInvalidation(scope, instance)
+
private fun invalidateChecked(
scope: RecomposeScopeImpl,
anchor: Anchor,
@@ -959,7 +968,7 @@
} else null
}
if (delegate == null) {
- if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
+ if (tryImminentInvalidation(scope, instance)) {
// The invalidation was redirected to the composer.
return InvalidationResult.IMMINENT
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
index 24a3119..d5ad0d4 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
@@ -32,8 +32,10 @@
import kotlin.test.assertNotEquals
import kotlin.test.assertSame
import kotlin.test.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
@Stable
+@OptIn(ExperimentalCoroutinesApi::class)
class MovableContentTests {
@Test
@@ -1470,6 +1472,39 @@
assertEquals(2, hashList.size)
assertEquals(hashList[0], hashList[1])
}
+
+ @Test
+ fun parameterPassingThroughDeferredSubcompose() = compositionTest {
+ var state by mutableStateOf(false)
+ var lastSeen: Boolean? = null
+ val content = movableContentOf { parameter: Boolean ->
+ Container {
+ lastSeen = parameter
+ }
+ }
+
+ compose {
+ if (state) {
+ content(true)
+ } else {
+ DeferredSubcompose {
+ content(state)
+ }
+ }
+ }
+
+ testCoroutineScheduler.advanceTimeBy(5_000)
+
+ assertEquals(state, lastSeen)
+
+ repeat(5) {
+ state = !state
+
+ expectChanges()
+
+ assertEquals(state, lastSeen, "Failed in iteration $it")
+ }
+ }
}
@Composable
@@ -1593,7 +1628,6 @@
composition.setContent(content)
}
DisposableEffect(Unit) {
-
onDispose { composition.dispose() }
}
}
@@ -1622,14 +1656,14 @@
}
override fun onForgotten() {
- check(count > 0) { "Abandoned or forgotten mor times than remembered" }
+ check(count > 0) { "Abandoned or forgotten more times than remembered" }
forgottenCount++
count--
if (count == 0) died = true
}
override fun onAbandoned() {
- check(count > 0) { "Abandoned or forgotten mor times than remembered" }
+ check(count > 0) { "Abandoned or forgotten more times than remembered" }
abandonedCount++
count--
if (count == 0) died = true