Git Product home page Git Product logo

molecule's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

molecule's Issues

Unit test iOS/tvOS frame clock

From #170

This requires we link against XCTest so as to be able to display an application and trigger the frame pulse.

Some experiments in jw.xctest.2023-01-31 branch.

Integration issue with kotlinx.serialization plugin: kotlinx.serialization compiler plugin internal error: unable to transform declaration

Molecule version: 0.7.0
Kotlin version: 1.8.0, 1.8.10
Kotlinx Serialisation: 1.5.0-RC

> Task :shared:compileKotlinIosSimulatorArm64 FAILED
e: Compilation failed: Back-end: Please report this problem https://kotl.in/issue
/Users/TFNX46/Developer/MULTIPLATFORM/KotlinXIssues/shared/src/commonMain/kotlin/io/github/xxfast/kotlinx/issues/Models.kt:5:1
Problem with `@Serializable
@StabilityInferred(parameters = 0)
sealed class EntitySearchResult {
  protected constructor() /* primary */ {
    super/*Any*/()
    /* <init>() */

  }

  companion object Companion : SerializerFactory {
    private constructor() /* primary */ {
      super/*Any*/()
      /* <init>() */

    }

    @GCUnsafeCall(callee = "Kotlin_Any_equals")
    external /* fake */ override operator fun equals(other: Any?): Boolean
    /* fake */ override fun hashCode(): Int
    /* fake */ override fun toString(): String
    fun serializer(): KSerializer<EntitySearchResult>
    override fun serializer(vararg typeParamsSerializers: KSerializer<*>): KSerializer<*>
  }

  @GCUnsafeCall(callee = "Kotlin_Any_equals")
  external /* fake */ override operator fun equals(other: Any?): Boolean
  /* fake */ override fun hashCode(): Int
  /* fake */ override fun toString(): String
  @Deprecated(message = "This synthesized declaration should not be used directly", replaceWith = ReplaceWith(expression = "", imports = []), level = DeprecationLevel.HIDDEN)
  constructor(seen1: Int, serializationConstructorMarker: SerializationConstructorMarker?) 
  private val $stableprop: Int
    field = 0

}

`
Details: kotlinx.serialization compiler plugin internal error: unable to transform declaration, see cause

 * Source files: Models.kt
 * Compiler version info: Konan: 1.8.10 / Kotlin: 1.8.10
 * Output kind: LIBRARY

This took me a while to figure out what exactly was causing this problem. I've raised this issue on kotlinx.serialization issue tracker but from what they mentioned, this was due to

In short, compose plugin produces incorrect IR which serialization plugin is unable to work with

I was originally suspecting it was compose-multiplatform that was causing this issue (given that there is a similar issue), but after isolating the issue to a standalone project - i was able to pinpoint this to molecule, or the version of compose compiler plugin that used by molecule gradle plugin, is what causing this issue.

Full stacktace.log
Standalone project to reproduce the issue: KotlinXIssues.zip

Flaky test: MoleculeTest.errorDelayed

 app.cash.molecule.MoleculeTest > errorDelayed FAILED
    java.lang.AssertionError: expected:<1> but was:<0>
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:120)
        at org.junit.Assert.assertEquals(Assert.java:146)
        at app.cash.molecule.MoleculeTest.errorDelayed(MoleculeTest.kt:126)

Kotlin 1.7 support

Tracking issue.

  • Google needs to land 1.7 into AOSP
  • Google needs to release stable version with 1.7 (1.2.0)
  • JetBrains needs to release stable version with 1.7 pointing at Google's stable version.

Fill out KDoc more

Bare minimum added in #52 so we could flip the public bit and push the first preview (after only 5 months). The KDoc should subsume or mirror some of the README content about clocks.

StateFlow support?

Would be better for Compose UI usage, but also means we need a CoroutineScope / CoroutineContext into which we can boostrap the composition.

State production suspension and caching with Molecule

There are two questions in this:

  1. suspending a launched Molecule: CoroutineScope.launchMolecule implies in name and verifies in source that the launched Molecule will keep producing state while the launching Coroutine is active. In the case where there is no collector of the produced state, can there be a way to stop composing the Presenter?
  2. If the above can be accommodated, can there be an API to seed the last produced state to presenters? This is so that resuming collecting from the presenter does not overwrite the last produced state value.

Consider the ProfilePresenter example:

@Composable
fun ProfilePresenter(
  userFlow: Flow<User>,
  balanceFlow: Flow<Long>,
): ProfileModel {
  val user by userFlow.collectAsState(null)
  val balance by balanceFlow.collectAsState(0L)

  return if (user == null) {
    Loading
  } else {
    Data(user.name, balance)
  }
}

The initial state is produced will always be Loading. In the case where Data has been produced and the ProfilePresenter is no longer being composed because the StateFlow is not being collected from, (maybe the owning screen has been placed in the back stack), I would like to prevent the last seen Data from immediately being overwritten by Loading as I return to the screen and the presenter produces state yet again.

i.e:

  1. Screen in focus. State: Loading.
  2. Data emitted. State: Data.
  3. Screen in back stack, presenter is no longer composing. State: Data.
  4. Screen back in focus, presenter is recomposed. Last seen Data state is overwritten. State: Loading.

The above can be worked around by using moleculeFlow with the Flow builder to allow seeding as the Flow is cold, and then creating a StateFlow with the right SharingStarted argument:

class SeededStateHolder {
    var seed: ProfileModel = Loading
    val profileStateFlow = flow {
        // Pass the seed to the presenter to prevent overwriting state
        emitAll(moleculeFlow(ProfilePresenter(seed, ....))
    }
    // update the seed on each emission
    .onEach { seed = it }
    .stateIn(...)
}

Though I wonder if I'm missing something more obvious. If not, a built in API that allows for it would be really nice.

Re-add Flow's tests

I omitted them from #51 because our internals have changed significantly and my testing approach then was wrong. This issue tracks re-introducing the tests

Method Resolution failing across modules when @Composable fun has a @Composable lambda and called within launchMolecule

Reproducing project is here: https://github.com/steve-the-edwards/reproductions/tree/main/overridetest

abstract class AndroidLibAbstractClass<I1, I2, T> {
  @Composable
  abstract fun AComposableWithLambda(
    input1: I1,
    input2: I2,
    hoistState: @Composable (T) -> Unit
  ): Unit

  @Composable
  abstract fun AComposableWithoutLambda(
    input1: I1,
    input2: I2,
  ): Unit
}

@Composable fun <I1, I2, T> AndroidLibComposableWithLambda(
  i1: I1,
  i2: I2,
  objectWithComposables: AndroidLibAbstractClass<I1, I2, T>
): T? {
  val payload: MutableState<T?> = remember { mutableStateOf(null) }
  objectWithComposables.AComposableWithLambda(i1, i2) @Composable {
    payload.value = it
  }
  return payload.value
}

@Composable fun <I1, I2, T> AndroidLibComposableWithoutLambda(
  i1: I1,
  i2: I2,
  objectWithComposables: AndroidLibAbstractClass<I1, I2, T>
): Unit {
  objectWithComposables.AComposableWithoutLambda(i1, i2)
}

Are defined in android-lib-module.

In the app module, a concrete child class of AndroidLibAbstractClass can be used successfully in a Compose UI composition.

However, in the launchMolecule composition (see MethodResolutionTest) this fails on AndroidLibComposableWithLambda:

  private class AndroidAppConcreteTestClass(
    private val payload: String
  ) : AndroidLibAbstractClass<Unit, String, String>() {
    @Composable
    public override fun AComposableWithLambda(
      input1: Unit,
      input2: String,
      hoistState: @Composable (s: String) -> Unit
    ) {
      println("Can you hear me now? $payload")
      hoistState(payload + input2)
    }

    @Composable
    override fun AComposableWithoutLambda(
      input1: Unit,
      input2: String
    ) {
    }
  }

  @Test fun testMethodResolution() {
    val objectUnderTest = AndroidAppConcreteTestClass("a test")
    val broadcastFrameClock = BroadcastFrameClock {}
    val testScope = CoroutineScope(broadcastFrameClock)

    val testFlow = testScope.launchMolecule {
      AndroidLibComposableWithoutLambda(Unit, " again", objectUnderTest)
      AndroidLibComposableWithLambda(Unit, " again", objectUnderTest)
    }

    assert(testFlow.value.contentEquals("a test again"))
  }

because it gets an AbstractMethodError as it cannot resolve the concrete class's override at runtime?

Originally I thought it might be because I was using the 0.3.0-SNAPSHOT and the common KMP artifacts so I include a module with that example as well, but I was able to reproduce it with just 0.2.0 while all in Android/JVM.

Can it be considered stable(functionality)?

I know that latest release (0.1) does not signify stable release and that API may change considerably moving forward.
I want to know if Molecule is stable enough for production use (in terms of functionality, not the API).

I ask this because Molecule uses Compose engine(not Compose UI), which is already considered stable. As I read here, its already being used in Cash App.

Also, in terms of API, do you expect a lot to change inside the Composable functions? or is just the Composable functions to Flow API that may change?

Scheduler breaks keyboard behavior for Compose UI consumers

Compose UI's CoreTextField is very picky about getting immediate updates to its values. The common (but useless) example seen in the docs, where the text state is held in a by remember { mutableStateOf("") }, has no problems updating the text field when typing on the keyboard quickly. However, if you provide the text value from something that doesn't provide updates synchronously from onValueChange, the text field starts behaving erratically; characters randomly disappear, the cursor moves around in its own, and other weird stuff.

This issue made us realize that mapping UI state on a background thread will never work. So we've been running our presenter composition on the main thread, using an immediate monotonic frame clock:

private object ImmediateMonotonicFrameClock : MonotonicFrameClock {
  override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
    return onFrame(System.nanoTime())
  }
}

This completely solved the keyboard glitchiness.

We're now looking to migrate to Molecule. But in doing so, we've seen the keyboard glitchiness return!
I somewhat narrowed it down to behaviors that depend on the combination of the Molecule's Dispatcher and MonotonicFrameClock:

Dispatcher MonotonicFrameClock Behavior
AndroidUiDispatcher Choreographer Bad
AndroidUiDispatcher ImmediateMonotonicFrameClock Less bad (but still bad)
Dispatchers.Main Choreographer Bad
Dispatchers.Main ImmediateMonotonicFrameClock Good

More specifics on the glitchiness:

Try spamming a character quickly. Occasionally, some will be dropped. When that happens, this appears in logcat:

getSurroundingText on inactive InputConnection
beginBatchEdit on inactive InputConnection
getTextBeforeCursor on inactive InputConnection
getTextAfterCursor on inactive InputConnection
getSelectedText on inactive InputConnection
endBatchEdit on inactive InputConnection

An easy way to check if the issue is happening is to hold the backspace button when there's some text present. With the issue, the backspace will randomly "stop working"; characters will stop being deleted even though you're still pressing backspace.

Honestly, I'm not positive as to whether this is Molecule's or CoreTextField's fault. Input appreciated.

Repro project with those dispatcher/frameclock modes

Testing Android ViewModels that use viewModelScope + Molecule may affect other tests

Discussed in #118

Originally posted by mhernand40 September 18, 2022
Been playing around with Molecule in my team's Android project by trying to introduce it as an implementation detail of two View Models; one that extends Jetpack's ViewModel and uses viewModelScope, and another that does not extend Jetpack's ViewModel and accepts any CoroutineScope via the constructor.

When it came to running the tests for each View Model, the tests passed when each test class was run in isolation. However, when running all the tests in one run, the test class for the View Model that extends Jetpack's ViewModel runs before the test class for the View Model that is a plain class, causing the latter's tests to fail.

It is worth noting the following:

  • The tests use runTest { โ€ฆ } from the Coroutines Test library with the default StandardTestDispatcher,
  • viewModelScope is used for the Jetpack ViewModel
    • Requires Dispatchers.setMain(โ€ฆ)/Dispatchers.resetMain()
    • the tests do not explicitly clear the ViewModel
    • the tests do not explicitly cancel viewModelScope
  • TestScope(testScheduler) is used for the View Model that does not extend Jetpack's ViewModel
    • No usage of Dispatchers.setMain(โ€ฆ)/Dispatchers.resetMain()

I have reduced the repro down to the following test class (no Jetpack ViewModel required):

internal class Repro {

  // This test passes but causes the next test to fail.
  @Test
  fun test1() {
    try {
      Dispatchers.setMain(StandardTestDispatcher())
      runTest {
        val moleculeScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        doRunTest(moleculeScope)
        // moleculeScope.cancel() // Uncommenting this fixes the test2 failure.
      }
    } finally {
      Dispatchers.resetMain()
    }
  }

  // This test only passes when run by itself or if it runs before test1.
  // If you rename this to test0 so that it runs before test1, it will pass.
  @Test
  fun test2() = runTest {
    val moleculeScope = TestScope(testScheduler)
    doRunTest(moleculeScope)
  }

  private fun TestScope.doRunTest(moleculeScope: CoroutineScope) {
    val event = MutableSharedFlow<String>(extraBufferCapacity = 1)
    val state = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
      var value by remember { mutableStateOf("") }
      LaunchedEffect(event) { event.collect { value = it } }
      value
    }
    runCurrent()
    event.tryEmit("test")
    runCurrent()
    assertEquals("test", state.value)
  }
}

test1 simulates the scenario when testing a Jetpack ViewModel that uses viewModelScope.

IDE does not resolve dependency added by molecule plugin in Common source set

I have tried to include the molecule plugin in my KMM project, but even after applying the plugin, it seems that the dependency is not added properly to all sourceSets. I have done some further research and the issue very similar to this issue opened here:

https://youtrack.jetbrains.com/issue/KTIJ-18425/

with the difference, that it uses "implementation" instead of "compileOnly".

I opened a new issue here with a minimal project for reproducing attached:

https://youtrack.jetbrains.com/issue/KTIJ-23680/IDE-does-not-resolve-dependency-added-by-KotlinCompilerPluginSupportPlugin-in-Common-code

Until this is resolved, you will have to add

           implementation("app.cash.molecule:molecule-runtime:0.6.0")

manually to the shared module build.gradle to make Molecule work in KMM projects.

Molecule does not work properly with TextField

Hi I have a simple setup like this

class ViewPresenter {
    data class ViewModel(val text: String, val isButtonEnabled: Boolean)
    private var text: String by mutableStateOf("")
    private val scope = CoroutineScope(job + app.cash.molecule.AndroidUiDispatcher.Main)
    val viewModel: StateFlow<ViewModel> = scope.launchMolecule(clock = ContextClock) {
           ViewModel(
		text = text,
		isButtonEnabled = text.isNullOrBlank().not()
	   )
    }

    fun onTextChanged(newText: String) {
          text = newText
    }
}


@Composable
fun View(presenter: ViewPresenter) {
	val model by presenter.viewModel.collectAsState()
	Column {
		TextField(
			value = model.text,
			onValueChanged = presenter::onTextChanged
		)
		Button(
    			onClick = {},
   			enabled = model.isButtonEnabled
		) {
    			Text("Login")
		}
	}
}

But it does not work properly. The TextField value never updated.

Unexpected behavior with counter example

I was trying out the counter example and I got unexpected results. I've done simple stuff with compose UI, so I could very well have made a simple mistake.

Here is the code I have:

class CounterActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(
            ComposeView(this).apply {
                setContent {
                    val scope = CoroutineScope(AndroidUiDispatcher.Main)
                    val count = scope.launchCounter().collectAsState()
                    Text(text = "${count.value}")
                }
            }
        )
    }
}

private fun CoroutineScope.launchCounter(): StateFlow<Int> = this.launchMolecule(RecompositionClock.ContextClock) {
    var count by remember {
        Log.d("test", "calculating initial count")
        mutableStateOf(0)
    }

    LaunchedEffect(Unit) {
        while(true) {
            delay(5000L)
            Log.d("test", "updating count")
            count++
        }
    }
    Log.d("test", "recomposing with count: $count")
    count
}

And my logcat looks like this:

D/test: calculating initial count
D/test: recomposing with count: 0
D/test: updating count
D/test: recomposing with count: 1
D/test: calculating initial count
D/test: recomposing with count: 0
D/test: calculating initial count
D/test: recomposing with count: 0
D/test: updating count
D/test: recomposing with count: 2
D/test: updating count
D/test: updating count
D/test: recomposing with count: 1
D/test: recomposing with count: 1
D/test: calculating initial count
D/test: recomposing with count: 0
D/test: calculating initial count
D/test: recomposing with count: 0
D/test: updating count
D/test: recomposing with count: 3
D/test: updating count
D/test: recomposing with count: 2
D/test: updating count
D/test: recomposing with count: 2
D/test: updating count
D/test: recomposing with count: 1
D/test: updating count
D/test: recomposing with count: 1
D/test: calculating initial count
D/test: recomposing with count: 0
D/test: calculating initial count
D/test: recomposing with count: 0

I was surprised that the UI wasn't showing the incrementing count, but I'm more surprised that the logs do show a count incrementing AND getting reset.

Changing a MutableState inside a coroutine seemingly leads to skipped emissions

I'm incrementing an initially 0 MutableState<Int> 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<Int> = 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)
        }
    }
}

How to access `LocalContext` or `stringResources` in a Presenter?

I am trying to access Context inside a presenter composable using LocalContext.Current, But I couldn't make it work. I am getting the following error ->

java.lang.IllegalStateException: CompositionLocal LocalContext not present
    at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.noLocalProvidedFor(AndroidCompositionLocals.android.kt:168)
    at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.access$noLocalProvidedFor(AndroidCompositionLocals.android.kt:1)
    at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$LocalContext$1.invoke(AndroidCompositionLocals.android.kt:54)
    at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$LocalContext$1.invoke(AndroidCompositionLocals.android.kt:53)
    at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
    at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29)
    at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31)
    at androidx.compose.runtime.ComposerImpl.resolveCompositionLocal(Composer.kt:2090)
    at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:2058)
    at com.dotmystyle.android.ui.feature.auth.LoginPresenterKt.LoginPresenter(LoginPresenter.kt:133)
    at com.dotmystyle.android.ui.feature.auth.LoginViewModel.models(LoginViewModel.kt:48)
    at com.dotmystyle.android.ui.feature.auth.LoginViewModel.models(LoginViewModel.kt:41)
    at com.dotmystyle.android.ui.core.presentation.BaseViewModel$models$2$1.invoke(BaseViewModel.kt:38)
    at com.dotmystyle.android.ui.core.presentation.BaseViewModel$models$2$1.invoke(BaseViewModel.kt:37)
    at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:164)
    at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:163)

Any advise or workaround it?

Document configuration for AGP to unit test Molecule

I'm trying out Molecule with AAC ViewModel. Here is how the VM code is written exposing stateFlow stream using launchMolecule()

Molecule version used : 0.5.0-SNAPSHOT

@HiltViewModel
class PhotosListViewModel @Inject constructor(
    private val unsplashRepository: UnsplashRepository,
    @MoleculeScope private val scope: CoroutineScope,
    @CompositionClock private val clock: RecompositionClock,
) : ViewModel() {

    private val events = Channel<Event>()
    val stateFlow = scope.launchMolecule(clock = clock) {
        present(events.receiveAsFlow())
    }

    init {
        processEvent(InitialPageEvent)
    }

    @Composable
    fun present(events: Flow<Event>): PhotosListUIState {
        var isLoading by remember { mutableStateOf(false) }
        var error by remember { mutableStateOf<String?>(null) }
        val images = remember { mutableStateListOf<UnsplashImage>() }

        LaunchedEffect(Unit) {
            events.collect { event ->
                when (event) {
                    InitialPageEvent -> {
                        isLoading = true
                        error = null
                        when (val result =
                            unsplashRepository.getPhotos(page = 1, perPage = ITEM_PER_PAGE)) {
                            is Error -> {
                                isLoading = false
                                error = result.message
                            }
                            is Success -> {
                                isLoading = false
                                images.addAll(result.data.images)
                            }
                        }

                    }
                }
            }
        }
        return PhotosListUIState(
            isLoading = isLoading,
            error = error,
            images = images,
        )
    }

    fun processEvent(event: Event) {
        scope.launch {
            events.send(event)
        }
    }
}

// UI State
data class PhotosListUIState(
    val isLoading: Boolean = false,
    val error: String? = null,
    val images: List<UnsplashImage> = emptyList(),
)

// Events
sealed interface Event
object InitialPageEvent : Event

App works fine on Android devices without any issue when VM is provided with correct coroutineScope and frameClock. However, when I write Unit test for VM it requires android.os.Trace to be mocked. Added both unit test and it's error log below.

VM's Unit Test

import app.cash.molecule.RecompositionClock
import app.cash.turbine.testIn
import dev.punitd.unplashapp.data.fake.FakeUnsplashRepository
import dev.punitd.unplashapp.model.images
import dev.punitd.unplashapp.model.pageLinks
import dev.punitd.unplashapp.util.CoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test


@OptIn(ExperimentalCoroutinesApi::class)
class PhotosListViewModelTest {

    @get:Rule
    val coroutineRule = CoroutineRule()

    @Test
    fun successStateIfApiSucceeds() = runTest {
        val viewModel = PhotosListViewModel(
            unsplashRepository = FakeUnsplashRepository(coroutineRule.testDispatcher),
            scope = this, // TestScope
            clock = RecompositionClock.Immediate,
        )

        // InitialPageEvent is send inside VM's init{} block by default
        // That's why we're not sending any events here.

        val turbine = viewModel.stateFlow.testIn(this)
        assertEquals(PhotosListUIState(isLoading = false), turbine.awaitItem())
        assertEquals(PhotosListUIState(isLoading = true), turbine.awaitItem())
        assertEquals(
            PhotosListUIState(
                isLoading = false,
                error = null,
                images = images,
                pageLinks = pageLinks
            ),
            turbine.awaitItem()
        )
        turbine.cancel()
    }
}

Test Error log

Method beginSection in android.os.Trace not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method beginSection in android.os.Trace not mocked. See http://g.co/androidstudio/not-mocked for details.
	at android.os.Trace.beginSection(Trace.java)
	at androidx.compose.runtime.Trace.beginSection(ActualAndroid.android.kt:30)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:4357)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3119)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:584)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:811)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
	at app.cash.molecule.MoleculeKt.launchMolecule(molecule.kt:163)
	at app.cash.molecule.MoleculeKt.launchMolecule(molecule.kt:108)
	at dev.punitd.unplashapp.screen.photos.PhotosListViewModel.<init>(PhotosListViewModel.kt:32)

I'm sure i'm doing something stupid here because unit tests written in sample app of this repo works just fine without any such error.

Questions:

  • Is there something I need to mock which i'm missing here?
  • Are Coroutinescope and FrameClock passed to VM in test incorrect?

Repo to reproduce it : https://github.com/punitda/Tinysplash

Thanks!

Relationship between molecule and Compose UI

Considering the common confusion around Compose / Compose UI it might be helpful to add a small section to the README explaining the relationship between molecule and Compose UI:

  • You don't need to use Compose UI in order to use molecule
  • If your app is fully Compose UI you don't necessarily need to use molecule
    • If you don't need the output to be a StateFlow you don't need molecule and can call "presenter" functions (from Compose UI's composition) that return a State<T>
    • <Are there any pros to using molecule in this scenario?>
    • <Are there any cons to using molecule in this scenario?>
      • <The only one I can think of is increased complexity (however small) by introducing a new dependency, having an additional composition, etc...>

Remove molecule-testing artifact

This is a tracking issue for removing the 'molecule-testing' artifact. You can test a Molecule with the Immediate clock and Turbine.

Do you have a use case which is only covered by the 'molecule-testing' artifact and not Turbine? Let us know here.

"Trying to call 'getOrThrow' on a failed channel result: Failed" when testing moleculeFlow with Turbine

Presenter:

@Composable fun PortfolioPresenter(): PortfolioModel {
  var portfolioModel by remember { mutableStateOf<PortfolioModel>(PortfolioModel.Loading) }
  LaunchedEffect("get-portfolio") {
    portfolioModel = PortfolioModel.Error("Boom!")
  }
  return portfolioModel
}

Test:

class PortfolioPresenterTest {
  @Test fun `loads portfolio`() = runBlocking {
    makePresenter().test {
      assertThat(awaitItem()).isEqualTo(PortfolioModel.Loading)
      assertThat(awaitItem()).isEqualTo(PortfolioModel.Error("Boom!"))
    }
  }

  private fun makePresenter(): Flow<PortfolioModel> {
    return moleculeFlow(clock = Immediate) {
      PortfolioPresenter()
    }
  }
}

The test fails with app.cash.turbine.AssertionError: Expected item but found Error(IllegalStateException), full stack trace below:

Stacktrace
Expected item but found Error(IllegalStateException)
app.cash.turbine.AssertionError: Expected item but found Error(IllegalStateException)
	at app//app.cash.turbine.ChannelBasedFlowTurbine.unexpectedEvent(FlowTurbine.kt:353)
	at app//app.cash.turbine.ChannelBasedFlowTurbine.awaitItem(FlowTurbine.kt:301)
	at app//dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest$loads portfolio$1$1.invokeSuspend(PortfolioPresenterTest.kt:14)
	at app//dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest$loads portfolio$1$1.invoke(PortfolioPresenterTest.kt)
	at app//dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest$loads portfolio$1$1.invoke(PortfolioPresenterTest.kt)
	at app//app.cash.turbine.FlowTurbineKt$test$4.invokeSuspend(FlowTurbine.kt:103)
	at app//app.cash.turbine.FlowTurbineKt$test$4.invoke(FlowTurbine.kt)
	at app//app.cash.turbine.FlowTurbineKt$test$4.invoke(FlowTurbine.kt)
	at app//kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
	at app//kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
	at app//app.cash.turbine.FlowTurbineKt.test(FlowTurbine.kt:101)
	at app//dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest$loads portfolio$1.invokeSuspend(PortfolioPresenterTest.kt:13)
	at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
	at app//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at app//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at app//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at app//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at app//dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest.loads portfolio(PortfolioPresenterTest.kt:12)
	at java.base@11.0.16/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base@11.0.16/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base@11.0.16/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base@11.0.16/java.lang.reflect.Method.invoke(Method.java:566)
	at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at app//org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base@11.0.16/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base@11.0.16/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base@11.0.16/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base@11.0.16/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.IllegalStateException: Trying to call 'getOrThrow' on a failed channel result: Failed
	at kotlinx.coroutines.channels.ChannelResult.getOrThrow-impl(Channel.kt:443)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1$1.invoke(molecule.kt:68)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1$1.invoke(molecule.kt:64)
	at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:164)
	at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:163)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
	at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
	at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2404)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2585)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
	at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2547)
	at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:620)
	at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:786)
	at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:105)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:456)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:425)
	at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42)
	at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71)
	at app.cash.molecule.GatedFrameClock.sendFrame(GatedFrameClock.kt:50)
	at app.cash.molecule.GatedFrameClock.access$sendFrame(GatedFrameClock.kt:31)
	at app.cash.molecule.GatedFrameClock$1.invokeSuspend(GatedFrameClock.kt:36)
	(Coroutine boundary)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend(Recomposer.kt:425)
	at androidx.compose.runtime.Recomposer$recompositionRunner$2$2.invokeSuspend(Recomposer.kt:682)
	at androidx.compose.runtime.Recomposer$recompositionRunner$2.invokeSuspend(Recomposer.kt:681)
	at app.cash.molecule.MoleculeKt$launchMolecule$2$1.invokeSuspend(molecule.kt:145)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1.invokeSuspend(molecule.kt:59)
	at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:230)
	at app.cash.turbine.FlowTurbineKt$collectTurbineIn$collectJob$1.invokeSuspend(FlowTurbine.kt:146)
Caused by: java.lang.IllegalStateException: Trying to call 'getOrThrow' on a failed channel result: Failed
	at kotlinx.coroutines.channels.ChannelResult.getOrThrow-impl(Channel.kt:443)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1$1.invoke(molecule.kt:68)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1$1.invoke(molecule.kt:64)
	at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:164)
	at app.cash.molecule.MoleculeKt$launchMolecule$2$3.invoke(molecule.kt:163)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
	at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
	at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2404)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2585)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
	at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2547)
	at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:620)
	at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:786)
	at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:105)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:456)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:425)
	at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42)
	at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71)
	at app.cash.molecule.GatedFrameClock.sendFrame(GatedFrameClock.kt:50)
	at app.cash.molecule.GatedFrameClock.access$sendFrame(GatedFrameClock.kt:31)
	at app.cash.molecule.GatedFrameClock$1.invokeSuspend(GatedFrameClock.kt:36)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:69)
	at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:245)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:161)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
	at androidx.compose.runtime.Recomposer.invalidate$runtime_release(Recomposer.kt:895)
	at androidx.compose.runtime.CompositionImpl.invalidate(Composition.kt:700)
	at androidx.compose.runtime.RecomposeScopeImpl.invalidateForResult(RecomposeScopeImpl.kt:148)
	at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked$invalidate(Composition.kt:552)
	at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked(Composition.kt:567)
	at androidx.compose.runtime.CompositionImpl.drainPendingModificationsLocked(Composition.kt:459)
	at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:658)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:763)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:433)
	at app.cash.molecule.MoleculeKt.launchMolecule(molecule.kt:163)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1.invokeSuspend(molecule.kt:64)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1.invoke(molecule.kt)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1.invoke(molecule.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:112)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1.invokeSuspend(molecule.kt:63)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1.invoke(molecule.kt)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1.invoke(molecule.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1.invokeSuspend(molecule.kt:59)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1.invoke(molecule.kt)
	at app.cash.molecule.MoleculeKt$immediateClockFlow$1.invoke(molecule.kt)
	at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61)
	at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:230)
	at app.cash.turbine.FlowTurbineKt$collectTurbineIn$collectJob$1.invokeSuspend(FlowTurbine.kt:146)
	at app.cash.turbine.FlowTurbineKt$collectTurbineIn$collectJob$1.invoke(FlowTurbine.kt)
	at app.cash.turbine.FlowTurbineKt$collectTurbineIn$collectJob$1.invoke(FlowTurbine.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:112)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at app.cash.turbine.FlowTurbineKt.collectTurbineIn(FlowTurbine.kt:143)
	at app.cash.turbine.FlowTurbineKt.access$collectTurbineIn(FlowTurbine.kt:1)
	at app.cash.turbine.FlowTurbineKt$test$4.invokeSuspend(FlowTurbine.kt:102)
	at app.cash.turbine.FlowTurbineKt$test$4.invoke(FlowTurbine.kt)
	at app.cash.turbine.FlowTurbineKt$test$4.invoke(FlowTurbine.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
	at app.cash.turbine.FlowTurbineKt.test(FlowTurbine.kt:101)
	at dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest$loads portfolio$1.invokeSuspend(PortfolioPresenterTest.kt:13)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at dev.egorand.moleculegetorthrowbug.PortfolioPresenterTest.loads portfolio(PortfolioPresenterTest.kt:12)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Repro project: https://github.com/Egorand/molecule-get-or-throw-bug

To reproduce the issue, run ./gradlew app:testDebugUnitTest.

JS frame time can produce the same value

AssertionFailedError: expected to be less than:<57000000L> but was:<57000000L>
	at <global>.fail(/System/Volumes/Data/home/circleci/code/assertk/src/commonMain/kotlin/assertk/failure.kt:159)
	at <global>.expected(/System/Volumes/Data/home/circleci/code/assertk/src/commonMain/kotlin/assertk/assertions/support/support.kt:99)
	at <global>.isLessThan(/System/Volumes/Data/home/circleci/code/assertk/src/commonMain/kotlin/assertk/assertions/comparable.kt:25)
	at <global>.<unknown>(/Volumes/dev/cashapp/molecule/molecule-runtime/src/commonTest/kotlin/app/cash/molecule/GatedFrameClockTest.kt:33)
	at <global>.all(/System/Volumes/Data/home/circleci/code/assertk/src/commonMain/kotlin/assertk/failure.kt:181)
	at protoOf.doResume_5yljmg(/Volumes/dev/cashapp/molecule/molecule-runtime/src/commonTest/kotlin/app/cash/molecule/GatedFrameClockTest.kt:31)
	at protoOf.resumeWith_7onugl(/Volumes/dev/cashapp/molecule-runtime/build/compileSync/js/test/testDevelopmentExecutable/kotlin/commonMainSources/libraries/stdlib/src/kotlin/util/Standard.kt:55)
	at protoOf.resumeWith_s3a3yh(molecule-molecule-runtime-test.1609909856.js:47269)
	at protoOf.run_mw4iiu(/Volumes/dev/cashapp/molecule-runtime/build/compileSync/js/test/testDevelopmentExecutable/kotlin/commonMainSources/libraries/stdlib/src/kotlin/coroutines/Continuation.kt:45)
	at protoOf.processEvent_iukd42(/mnt/agent/work/44ec6e850d5c63f0/kotlinx-coroutines-test/common/src/TestDispatcher.kt:28)

Need to track the last seen frame time and +1 if it matches.

Multiplatform support

Shipped:

  • Android (all versions)
  • JS (0.3.0 and newer)
  • JVM (0.3.0 and newer)
  • iOS (0.5.0-beta01 and newer)
  • MacOS (0.5.0-beta01 and newer)
  • tvOS (0.5.0-beta01 and newer)
  • watchOS (0.5.0-beta01 and newer)
  • Linux (0.5.0-beta01 and newer)
  • Windows (0.5.0-beta01 and newer)

sample-viewmodel module showcases keeping an always hot StateFlow in the ViewModel

This is an issue due to how AAC ViewModel typically is used. The backstack of screens keep the ViewModels in the backstack in memory, relying on behavior like stateIn and collectAsStateWithLifecycle() to turn the "hot" flows into "cold" ones when there no longer are observers on those states.

The example inside molecule could be altered to show a way which developers who still use ViewModels can adopt in their own apps without this pretty important downside of keeping all molecule StateFlows always hot in the backstack.

This was briefly discussed already in #271, but I do believe that it'd be worth it for the sample to be updated to solve this issue.

Recomposition happens twice!

I just pushed source code of a sample here. Everything looks good, except that if you run the code, and click the buttons, you'll see in the Logcat that recomposition is happening twice, once because we launched an action, which is expected, but the other one appeared to happen because of changing of the return value of the nested compose, which is unexpected! I made a workaround for the "previous page" button by launching two actions in a row!

    sealed interface PopStack : MainAction {
        object Flip : PopStack
        object Flop : PopStack
    }

To clarify the problem, I created this commit, so just check out to the-infinit-loop-problem branch. In this branch, after clicking the "previous page" button, it'll call the MainPresenter in some kind of a loop until the stack becomes empty! How can I avoid that infinit loop? Pull requests are welcome. By the way, thanks guys for this awesome library.

Zero allocation emit/skip indirection

In creating #4 I spent a lot of time trying to make it zero-allocation. While it's possible, I just want to land the behavior change before going to extreme lengths to avoid allocation.

Sketch:

/**
 * Models the union type of `T | Skip` which indicates the action to be performed after each
 * recomposition of a molecule function.
 *
 * @see [moleculeFlow]
 */
@JvmInline
@Suppress("unused") // This T forces Emit type param to match Flow type param.
value class Action<out T>
@PublishedApi internal constructor(
  @PublishedApi internal val value: Any?
)

/** Molecule action which emits an item to the underlying stream. */
@Suppress("FunctionName", "NOTHING_TO_INLINE") // Type-like factory function.
inline fun <T> Emit(item: T): Action<T> = Action(item)

@JvmField
@PublishedApi
internal val skipMarker = Any()

/** Molecule action which skips emission to the underlying stream. */
val Skip: Action<Nothing> = Action(skipMarker)

suspend inline fun <T> molecule(
  noinline emit: suspend (T) -> Unit,
  crossinline body: @Composable () -> Action<T>,
) {
  molecule(emit) { items ->
    val value = body().value
    if (value !== skipMarker) {
      @Suppress("UNCHECKED_CAST")
      items(value as T)
    }
  }
}

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

This repository currently has no open or pending branches.

Detected dependencies

github-actions
.github/workflows/build.yaml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v3
  • reactivecircus/android-emulator-runner v2
.github/workflows/gradle-wrapper.yaml
  • actions/checkout v4
  • gradle/actions v3
.github/workflows/release.yaml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v3
  • ffurrer2/extract-release-notes v2
  • softprops/action-gh-release v2
gradle
gradle.properties
settings.gradle
build.gradle
gradle/libs.versions.toml
  • com.android.tools.build:gradle 8.5.1
  • androidx.core:core-ktx 1.13.1
  • androidx.test:runner 1.6.1
  • androidx.activity:activity-compose 1.9.0
  • androidx.compose:compose-bom 2024.06.00
  • androidx.compose.compiler:compiler 1.5.14
  • io.coil-kt:coil-compose 2.6.0
  • org.jetbrains.compose.runtime:runtime 1.6.11
  • org.jetbrains.dokka:dokka-gradle-plugin 1.9.20
  • junit:junit 4.13.2
  • org.jetbrains.kotlin:kotlin-gradle-plugin 2.0.0
  • org.jetbrains.kotlin:compose-compiler-gradle-plugin 2.0.0
  • org.jetbrains.kotlin:kotlin-test 2.0.0
  • com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin 2.0.0-1.0.23
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.8.1
  • org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.1
  • org.jetbrains.kotlinx:binary-compatibility-validator 0.15.1
  • com.pinterest.ktlint:ktlint-cli 1.3.1
  • io.nlopez.compose.rules:ktlint 0.4.5
  • com.vanniktech:gradle-maven-publish-plugin 0.29.0
  • com.diffplug.spotless:spotless-plugin-gradle 6.25.0
  • com.squareup.okhttp3:okhttp 4.12.0
  • com.squareup.okhttp3:logging-interceptor 4.12.0
  • com.squareup.retrofit2:retrofit 2.11.0
  • com.squareup.retrofit2:converter-scalars 2.11.0
  • com.squareup.retrofit2:converter-moshi 2.11.0
  • com.squareup.moshi:moshi-kotlin-codegen 1.15.1
  • com.willowtreeapps.assertk:assertk 0.28.1
  • app.cash.turbine:turbine 1.1.0
molecule-runtime/gradle.properties
molecule-runtime/build.gradle
sample/build.gradle
sample-viewmodel/build.gradle
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.9

  • Check this box to trigger a request for Renovate to run again on this repository

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.