Merge "Send onAbandoned when exceptions occur during composition" into androidx-main
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 217bba6..017b992 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
@@ -1590,12 +1590,15 @@
writer.update(value)
if (value is RememberObserver) {
record { _, _, rememberManager -> rememberManager.remembering(value) }
+ abandonSet.add(value)
}
} else {
val groupSlotIndex = reader.groupSlotIndex - 1
+ if (value is RememberObserver) {
+ abandonSet.add(value)
+ }
recordSlotTableOperation(forParent = true) { _, slots, rememberManager ->
if (value is RememberObserver) {
- abandonSet.add(value)
rememberManager.remembering(value)
}
when (val previous = slots.set(groupSlotIndex, value)) {
@@ -1621,9 +1624,6 @@
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun updateCachedValue(value: Any?) {
- if (inserting && value is RememberObserver) {
- abandonSet.add(value)
- }
updateValue(value)
}
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 fad5cd8..15f0c75 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
@@ -471,9 +471,11 @@
override fun composeContent(content: @Composable () -> Unit) {
// TODO: This should raise a signal to any currently running recompose calls
// to halt and return
- synchronized(lock) {
- drainPendingModificationsForCompositionLocked()
- composer.composeContent(takeInvalidations(), content)
+ trackAbandonedValues {
+ synchronized(lock) {
+ drainPendingModificationsForCompositionLocked()
+ composer.composeContent(takeInvalidations(), content)
+ }
}
}
@@ -482,13 +484,17 @@
if (!disposed) {
disposed = true
composable = {}
- if (slotTable.groupsSize > 0) {
+ val nonEmptySlotTable = slotTable.groupsSize > 0
+ if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
val manager = RememberEventDispatcher(abandonSet)
- slotTable.write { writer ->
- writer.removeCurrentGroup(manager)
+ if (nonEmptySlotTable) {
+ slotTable.write { writer ->
+ writer.removeCurrentGroup(manager)
+ }
+ applier.clear()
+ manager.dispatchRememberObservers()
}
- applier.clear()
- manager.dispatchRememberObservers()
+ manager.dispatchAbandons()
}
composer.dispose()
parent.unregisterComposition(this)
@@ -609,9 +615,11 @@
override fun recompose(): Boolean = synchronized(lock) {
drainPendingModificationsForCompositionLocked()
- composer.recompose(takeInvalidations()).also { shouldDrain ->
- // Apply would normally do this for us; do it now if apply shouldn't happen.
- if (!shouldDrain) drainPendingModificationsLocked()
+ trackAbandonedValues {
+ composer.recompose(takeInvalidations()).also { shouldDrain ->
+ // Apply would normally do this for us; do it now if apply shouldn't happen.
+ if (!shouldDrain) drainPendingModificationsLocked()
+ }
}
}
@@ -723,6 +731,19 @@
}
}
+ private inline fun <T> trackAbandonedValues(block: () -> T): T {
+ var success = false
+ return try {
+ block().also {
+ success = true
+ }
+ } finally {
+ if (!success && abandonSet.isNotEmpty()) {
+ RememberEventDispatcher(abandonSet).dispatchAbandons()
+ }
+ }
+ }
+
/**
* Helper for collecting remember observers for later strictly ordered dispatch.
*
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 d937d0c..9f873b7 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
@@ -44,10 +44,12 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
@@ -1943,6 +1945,109 @@
}
@Test
+ fun testRememberObserver_Abandon_Simple() = compositionTest {
+ val abandonedObjects = mutableListOf<RememberObserver>()
+ val observed = object : RememberObserver {
+ override fun onAbandoned() {
+ abandonedObjects.add(this)
+ }
+
+ override fun onForgotten() {
+ error("Unexpected call to onForgotten")
+ }
+
+ override fun onRemembered() {
+ error("Unexpected call to onRemembered")
+ }
+ }
+
+ assertFailsWith(IllegalStateException::class, message = "Throw") {
+ compose {
+ @Suppress("UNUSED_EXPRESSION")
+ remember { observed }
+ error("Throw")
+ }
+ }
+
+ assertArrayEquals(listOf(observed), abandonedObjects)
+ }
+
+ @Test
+ fun testRememberObserver_Abandon_Recompose() {
+ val abandonedObjects = mutableListOf<RememberObserver>()
+ val observed = object : RememberObserver {
+ override fun onAbandoned() {
+ abandonedObjects.add(this)
+ }
+
+ override fun onForgotten() {
+ error("Unexpected call to onForgotten")
+ }
+
+ override fun onRemembered() {
+ error("Unexpected call to onRemembered")
+ }
+ }
+ assertFailsWith(IllegalStateException::class, message = "Throw") {
+ compositionTest {
+ val rememberObject = mutableStateOf(false)
+
+ compose {
+ if (rememberObject.value) {
+ @Suppress("UNUSED_EXPRESSION")
+ remember { observed }
+ error("Throw")
+ }
+ }
+
+ assertTrue(abandonedObjects.isEmpty())
+
+ rememberObject.value = true
+
+ advance(ignorePendingWork = true)
+ }
+ }
+
+ assertArrayEquals(listOf(observed), abandonedObjects)
+ }
+
+ @Test
+ fun testRememberedObserver_Controlled_Dispose() = runBlocking {
+ val recomposer = Recomposer(coroutineContext)
+ val root = View()
+ val controlled = ControlledComposition(ViewApplier(root), recomposer)
+
+ val abandonedObjects = mutableListOf<RememberObserver>()
+ val observed = object : RememberObserver {
+ override fun onAbandoned() {
+ abandonedObjects.add(this)
+ }
+
+ override fun onForgotten() {
+ error("Unexpected call to onForgotten")
+ }
+
+ override fun onRemembered() {
+ error("Unexpected call to onRemembered")
+ }
+ }
+
+ controlled.composeContent {
+ @Suppress("UNUSED_EXPRESSION")
+ remember<RememberObserver> {
+ observed
+ }
+ }
+
+ assertTrue(abandonedObjects.isEmpty())
+
+ controlled.dispose()
+
+ assertArrayEquals(listOf(observed), abandonedObjects)
+ recomposer.close()
+ }
+
+ @Test
fun testCompoundKeyHashStaysTheSameAfterRecompositions() = compositionTest {
val outerKeys = mutableListOf<Int>()
val innerKeys = mutableListOf<Int>()