Changing a MutableState inside a coroutine seemingly leads to skipped emissions
- Dominant language
- Kotlin
- Stars
- 2.2k
- Forks
- 116
- Avg merge
- 1h 36m
- Merged PRs (30d)
- 8
Description
I'm incrementing an initially 0 `MutableState` twice in a row:
1. If done outside of a coroutine it works as expected and `myPresenter()` emits [0,1,2].
2. If done inside of a newly launched coroutine it emits only [0,2] (skipping the "1" state) .
Is this a bug or working as intended?
And if it is WAI, how come? I naively thought that in both cases I should expect my presenter to emit [0,1,2].
(running Kotlin 1.8.21, kotlinx.coroutines 1.7.1, compose-runtime 1.4.3, molecule 0.9.0, turbine 0.13.0)
```
package example.test
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
data class MyState(
val anInt: Int,
val eventSink: (MyEvent) -> Unit,
)
sealed interface MyEvent {
object Increment : MyEvent
object IncrementSuspending : MyEvent
}
@Composable fun myPresenter(): MyState {
val scope = rememberCoroutineScope()
val anInt: MutableState = remember { mutableStateOf(0) }
return MyState(anInt.value) {
when (it) {
MyEvent.Increment -> {
anInt.value++
anInt.value++
}
MyEvent.IncrementSuspending -> scope.launch {
anInt.value++
anInt.value++
}
}
}
}
class MoleculeTestCase {
@Test fun `process Increment event`() = runTest {
moleculeFlow(RecompositionClock.Immediate) { myPresenter() }.test {
awaitItem().apply {
assertEquals(0, anInt)
eventSink(MyEvent.Increment)
}
assertEquals(1, awaitItem().anInt)
assertEquals(2, awaitItem().anInt)
}
}
@Test fun `process IncrementSuspending event`() = runTest {
moleculeFlow(RecompositionClock.Immediate) { myPresenter() }.test {
awaitItem().apply {
assertEquals(0, anInt)
eventSink(MyEvent.IncrementSuspending)
}
assertEquals(2, awaitItem().anInt)
}
}
}
```
Contributor guide
No contributing guide indexed for this repository
Research direction
The reproduction is in MoleculeTestCase, with behavior exercised through myPresenter(), moleculeFlow, RecompositionClock.Immediate, and Turbine. Run both tests under runTest and trace the synchronous and launched event paths, especially when recomposition occurs. Done means the emission semantics are confirmed and, if incorrect, captured by a regression test or documented as intended.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin
- Domain
- testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100