Fix propagating invalidations to subcompositions
Writes to state objects in a parent composition were
not being correctly propagated to sub-compositions
which could cause, for example, lambda captures to be
out-of-date.
Test: ./gradles :compose:r:r:tDUT
Change-Id: I89d7863fa61e286761b53cadd2ffd16820b44317
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index e7b3a19..6db216b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -16,6 +16,7 @@
package androidx.compose.runtime
+import androidx.compose.runtime.collection.IdentityArraySet
import androidx.compose.runtime.snapshots.MutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotApplyResult
@@ -416,9 +417,12 @@
}
// Perform recomposition for any invalidated composers
+ val modifiedValues = IdentityArraySet<Any>()
try {
toRecompose.fastForEach { composer ->
- performRecompose(composer)?.let { toApply += it }
+ performRecompose(composer, modifiedValues)?.let {
+ toApply += it
+ }
}
if (toApply.isNotEmpty()) changeCount++
} finally {
@@ -494,7 +498,7 @@
content: @Composable () -> Unit
) {
val composerWasComposing = composition.isComposing
- composing(composition) {
+ composing(composition, null) {
composition.composeContent(content)
}
// TODO(b/143755743)
@@ -518,10 +522,14 @@
}
}
- private fun performRecompose(composition: ControlledComposition): ControlledComposition? {
+ private fun performRecompose(
+ composition: ControlledComposition,
+ modifiedValues: IdentityArraySet<Any>
+ ): ControlledComposition? {
if (composition.isComposing || composition.isDisposed) return null
return if (
- composing(composition) {
+ composing(composition, modifiedValues) {
+ modifiedValues.forEach { composition.recordWriteOf(it) }
composition.recompose()
}
) composition else null
@@ -531,13 +539,23 @@
return { value -> composition.recordReadOf(value) }
}
- private fun writeObserverOf(composition: ControlledComposition): (Any) -> Unit {
- return { value -> composition.recordWriteOf(value) }
+ private fun writeObserverOf(
+ composition: ControlledComposition,
+ modifiedValues: IdentityArraySet<Any>?
+ ): (Any) -> Unit {
+ return { value ->
+ composition.recordWriteOf(value)
+ modifiedValues?.add(value)
+ }
}
- private inline fun <T> composing(composition: ControlledComposition, block: () -> T): T {
+ private inline fun <T> composing(
+ composition: ControlledComposition,
+ modifiedValues: IdentityArraySet<Any>?,
+ block: () -> T
+ ): T {
val snapshot = Snapshot.takeMutableSnapshot(
- readObserverOf(composition), writeObserverOf(composition)
+ readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 69e8be0..9f0ecb3 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -2608,6 +2608,40 @@
validate()
}
+ @Test
+ fun testModificationsPropagateToSubcomposition() = compositionTest {
+ var value by mutableStateOf(0)
+ val content: MutableState<@Composable () -> Unit> = mutableStateOf({ })
+ @Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER")
+ var subCompositionOccurred = false
+
+ @Composable
+ fun ComposeContent() {
+ content.value()
+ }
+
+ fun updateContent(parentValue: Int) {
+ content.value = {
+ subCompositionOccurred = true
+ assertEquals(parentValue, value)
+ }
+ }
+
+ compose {
+ updateContent(value)
+ TestSubcomposition {
+ ComposeContent()
+ }
+ }
+
+ subCompositionOccurred = false
+
+ value = 10
+ expectChanges()
+
+ assertTrue(subCompositionOccurred)
+ }
+
/**
* This test checks that an updated ComposableLambda capture used in a subcomposition
* correctly invalidates that subcomposition and schedules recomposition of that subcomposition.