diff --git a/molecule/molecule-runtime/src/commonTest/java/app/cash/molecule/MoleculeTest.kt b/molecule/molecule-runtime/src/commonTest/java/app/cash/molecule/MoleculeTest.kt index c06b2c57..1a10a475 100644 --- a/molecule/molecule-runtime/src/commonTest/java/app/cash/molecule/MoleculeTest.kt +++ b/molecule/molecule-runtime/src/commonTest/java/app/cash/molecule/MoleculeTest.kt @@ -35,6 +35,12 @@ import org.junit.Test import kotlin.coroutines.CoroutineContext import kotlin.test.assertFailsWith import kotlin.time.ExperimentalTime +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout @ExperimentalCoroutinesApi @ExperimentalTime @@ -102,31 +108,33 @@ class MoleculeTest { override val key get() = CoroutineExceptionHandler } - @Test fun errorDelayed() { - val dispatcher = TestCoroutineDispatcher() + @Test fun errorDelayed() = runBlocking { val clock = BroadcastFrameClock() - val exceptionHandler = RecordingExceptionHandler() - val scope = CoroutineScope(dispatcher + clock + exceptionHandler) - var value: Int? = null + val values = Channel(UNLIMITED) // Use a custom subtype to prevent coroutines from breaking referential equality. val runtimeException = object : RuntimeException() {} var count by mutableStateOf(0) - scope.launchMolecule(emitter = { value = it }) { - if (count == 1) { - throw runtimeException - } - count + val exception = async { + runCatching { + withContext(clock) { + launchMolecule(emitter = values::trySend) { + if (count == 1) { + throw runtimeException + } + count + } + } + }.exceptionOrNull() } - assertEquals(0, value) + assertEquals(0, withTimeout(1000) { values.receive() }) count++ Snapshot.sendApplyNotifications() // Ensure external state mutation is observed. clock.sendFrame(0) - assertSame(runtimeException, exceptionHandler.exceptions.single()) - scope.cancel() + assertSame(runtimeException, withTimeout(1000) { exception.await() }) } @Test fun errorInEffect() { diff --git a/molecule/molecule-runtime/src/main/java/app/cash/molecule/molecule.kt b/molecule/molecule-runtime/src/main/java/app/cash/molecule/molecule.kt index ef921565..38d44673 100644 --- a/molecule/molecule-runtime/src/main/java/app/cash/molecule/molecule.kt +++ b/molecule/molecule-runtime/src/main/java/app/cash/molecule/molecule.kt @@ -24,10 +24,14 @@ import androidx.compose.runtime.snapshots.Snapshot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.job import kotlinx.coroutines.launch @@ -89,7 +93,12 @@ fun CoroutineScope.launchMolecule( body: @Composable () -> T, ) { val recomposer = Recomposer(coroutineContext) - val composition = Composition(UnitApplier, recomposer) + Composition(UnitApplier, recomposer).apply { + setContent { + emitter(body()) + } + } + launch(start = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } @@ -107,10 +116,6 @@ fun CoroutineScope.launchMolecule( coroutineContext.job.invokeOnCompletion { snapshotHandle.dispose() } - - composition.setContent { - emitter(body()) - } } private object UnitApplier : AbstractApplier(Unit) {