[go: nahoru, domu]

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>()