cashapp / molecule Goto Github PK
View Code? Open in Web Editor NEWBuild a StateFlow stream using Jetpack Compose
Home Page: https://cashapp.github.io/molecule/docs/1.x/
License: Apache License 2.0
Build a StateFlow stream using Jetpack Compose
Home Page: https://cashapp.github.io/molecule/docs/1.x/
License: Apache License 2.0
MoleculeTurbine can emit duplicate output values, which results in tests that assert non-stateful behavior.
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.
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
Not sure this is something that can be addressed easily because of this coroutines issue noted in MoleculeTestingTest
, but it would be great for tests that require time manipulation.
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)
Tracking issue.
So you can add the testing dependency without specifying a version
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.
Basically mirroring the problems we saw without pulling in the whole Rx dependency or using Rx.
The common cases, but also what happens when an emitter throws such as what can happen with the flow factory impl.
It fails constantly.
Replicate cashapp/redwood#686
Would be better for Compose UI usage, but also means we need a CoroutineScope / CoroutineContext into which we can boostrap the composition.
There are two questions in this:
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
?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:
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.
We should be testing on all platforms
I would like to use molecule on other targets as well: native targets, JS(IR) and plain jvm.
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
If you apply Jetbrains compose plugin manually too, this js build fails because generateDecoys is provided twice for JS (with the same value).
Small reproducer coming soon, sample app: https://github.com/hfhbd/ComposeTodo/pull/665/files
Workaround: just don't use this gradle plugin and depend on the runtime manually
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.
e: Module "moe.tlaster:precompose-molecule" has a reference to symbol app.cash.molecule/launchMolecule|-7305106088441339121[0]. Neither the module itself nor its dependencies contain such declaration.
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?
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 |
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.
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:
runTest { โฆ }
from the Coroutines Test library with the default StandardTestDispatcher
,viewModelScope
is used for the Jetpack ViewModel
Dispatchers.setMain(โฆ)
/Dispatchers.resetMain()
ViewModel
viewModelScope
TestScope(testScheduler)
is used for the View Model that does not extend Jetpack's ViewModel
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
.
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:
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.
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.
Blocked on:
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.
I'm incrementing an initially 0 MutableState<Int>
twice in a row:
myPresenter()
emits [0,1,2].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)
}
}
}
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?
Requires some changes to the test harness but should be doable
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:
Repo to reproduce it : https://github.com/punitda/Tinysplash
Thanks!
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:
StateFlow
you don't need molecule and can call "presenter" functions (from Compose UI's composition) that return a State<T>
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.
It seems that 0.9.0 has recently gone missing from maven central. I updated from 0.8.0 a few days ago. Yesterday my builds started failing while looking for molecule 0.9.0. It isn't listed when browsing maven central: https://central.sonatype.com/artifact/app.cash.molecule/molecule-runtime.
Tracking issue.
Blocked by JetBrains/compose-multiplatform#2349
I don't remember if JS does, but the iOS/MacOS/tvOS ones definitely don't.
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:
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
.
Requires a compatible version of Compose
Blocked by:
Tracking issue
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.
Shipped:
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.
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.
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)
}
}
}
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.
.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.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.properties
gradle 8.9
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.