cashapp / cashapp/molecule

Changing a MutableState inside a coroutine seemingly leads to skipped emissions

Open
#249 4 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.