Skip to content

Allow Dispatchers to be specified in collectAsState #721

@mr-thierry

Description

@mr-thierry

We are sometime seeing tests that fails with:

java.lang.IllegalStateException: The current thread must have a looper!

This seemed to be caused by Composition not happening on the main thread. This is the stacktrace:

java.lang.IllegalStateException: The current thread must have a looper!
	at android.view.Choreographer$1.initialValue(Choreographer.java:111)
	at android.view.Choreographer$1.initialValue(Choreographer.java:106)
	at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:180)
	at java.lang.ThreadLocal.get(ThreadLocal.java:170)
	at android.view.Choreographer.getInstance(Choreographer.java:296)
	at androidx.compose.foundation.lazy.layout.AndroidPrefetchScheduler.<init>(PrefetchScheduler.android.kt:108)
	at androidx.compose.foundation.lazy.layout.PrefetchScheduler_androidKt.rememberDefaultPrefetchScheduler(PrefetchScheduler.android.kt:38)
	at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3.invoke(LazyLayout.kt:90)
	at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3.invoke(LazyLayout.kt:82)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:118)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.foundation.lazy.layout.LazySaveableStateHolderKt$LazySaveableStateHolderProvider$1.invoke(LazySaveableStateHolder.kt:51)
	at androidx.compose.foundation.lazy.layout.LazySaveableStateHolderKt$LazySaveableStateHolderProvider$1.invoke(LazySaveableStateHolder.kt:49)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:401)
	at androidx.compose.foundation.lazy.layout.LazySaveableStateHolderKt.LazySaveableStateHolderProvider(LazySaveableStateHolder.kt:49)
	at androidx.compose.foundation.lazy.layout.LazyLayoutKt.LazyLayout(LazyLayout.kt:82)
	at androidx.compose.foundation.lazy.LazyListKt.LazyList(LazyList.kt:106)
	at androidx.compose.foundation.lazy.LazyDslKt.LazyColumn(LazyDsl.kt:368)
	at com.example.LazyColumn(LazyColumn.kt:205)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:118)
	at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$1.invoke(ComposableLambda.jvm.kt:130)
	at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$1.invoke(ComposableLambda.jvm.kt:129)
	at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:192)
	at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2825)
	at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:3116)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3607)
	at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3552)
	at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:948)
	at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1206)
	at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:132)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:616)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:585)
	at androidx.compose.ui.test.TestMonotonicFrameClock$withFrameNanos$2$1$1.invoke(TestMonotonicFrameClock.jvm.kt:104)
	at androidx.compose.ui.test.TestMonotonicFrameClock$withFrameNanos$2$1$1.invoke(TestMonotonicFrameClock.jvm.kt:103)
	at androidx.compose.ui.test.TestMonotonicFrameClock$performFrame$1.invoke(TestMonotonicFrameClock.jvm.kt:149)
	at androidx.compose.ui.test.TestMonotonicFrameClock$performFrame$1.invoke(TestMonotonicFrameClock.jvm.kt:132)
	at androidx.compose.ui.test.FrameDeferringContinuationInterceptor.runWithoutResumingCoroutines(FrameDeferringContinuationInterceptor.jvm.kt:60)
	at androidx.compose.ui.test.TestMonotonicFrameClock.performFrame(TestMonotonicFrameClock.jvm.kt:132)
	at androidx.compose.ui.test.TestMonotonicFrameClock.access$performFrame(TestMonotonicFrameClock.jvm.kt:53)
	at androidx.compose.ui.test.TestMonotonicFrameClock$withFrameNanos$2$1$2.invokeSuspend(TestMonotonicFrameClock.jvm.kt:110)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:359)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:358)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:124)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:52)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:43)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
	at androidx.compose.ui.test.TestMonotonicFrameClock.withFrameNanos(TestMonotonicFrameClock.jvm.kt:108)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend(Recomposer.kt:585)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at androidx.compose.ui.test.ApplyingContinuationInterceptor$SendApplyContinuation.resumeWith(ApplyingContinuationInterceptor.kt:65)
	at androidx.compose.ui.test.FrameDeferringContinuationInterceptor$FrameDeferredContinuation.resumeWith(FrameDeferringContinuationInterceptor.jvm.kt:194)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:165)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:154)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:470)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core(CancellableContinuationImpl.kt:504)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core$default(CancellableContinuationImpl.kt:493)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:359)
	at androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1.invoke(Recomposer.kt:1042)
	at androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1.invoke(Recomposer.kt:1026)
	at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1945)
	at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1960)
	at androidx.compose.runtime.snapshots.SnapshotKt.access$advanceGlobalSnapshot(Snapshot.kt:1)
	at androidx.compose.runtime.snapshots.Snapshot$Companion.sendApplyNotifications(Snapshot.kt:692)
	at androidx.compose.ui.test.ApplyingContinuationInterceptor$SendApplyContinuation.resumeWith(ApplyingContinuationInterceptor.kt:66)
	at androidx.compose.ui.test.FrameDeferringContinuationInterceptor$FrameDeferredContinuation.resumeWith(FrameDeferringContinuationInterceptor.jvm.kt:194)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:165)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:154)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:470)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core(CancellableContinuationImpl.kt:504)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core$default(CancellableContinuationImpl.kt:493)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:359)
	at kotlinx.coroutines.flow.SharedFlowImpl.tryEmit(SharedFlow.kt:414)
	at kotlinx.coroutines.flow.SharedFlowImpl.emit$suspendImpl(SharedFlow.kt:419)
	at kotlinx.coroutines.flow.SharedFlowImpl.emit(Unknown Source:0)
	at com.airbnb.mvrx.CoroutinesStateStore$flushQueuesOnce$2$1.invokeSuspend(CoroutinesStateStore.kt:87)
	at com.airbnb.mvrx.CoroutinesStateStore$flushQueuesOnce$2$1.invoke(Unknown Source:8)
	at com.airbnb.mvrx.CoroutinesStateStore$flushQueuesOnce$2$1.invoke(Unknown Source:4)
	at kotlinx.coroutines.selects.SelectImplementation$ClauseData.invokeBlock(Select.kt:846)
	at kotlinx.coroutines.selects.SelectImplementation.complete(Select.kt:715)
	at kotlinx.coroutines.selects.SelectImplementation.doSelectSuspend(Select.kt:456)
	at kotlinx.coroutines.selects.SelectImplementation.access$doSelectSuspend(Select.kt:251)
	at kotlinx.coroutines.selects.SelectImplementation$doSelectSuspend$1.invokeSuspend(Unknown Source:14)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
	at java.lang.Thread.run(Thread.java:920)

I could not determine why sometime the CoroutinesStateStore emits in a background coroutine. Could this be caused by the Channel hitting a buffer limit, and then resuming?

A search in the Android Issue Tracker didn't result in any bug that could be present in Compose

In any case, I was able to avoid this exception by specifying the context in collectAsState:

@Composable
fun <VM : MavericksViewModel<S>, S : MavericksState> VM.collectAsState(): State<S> {
    return stateFlow.collectAsState(initial = withState(this) { it }, context = Dispatchers.Main)
}

As such I suggest that we modify collectAsState() to allow the caller to be able to specify a coroutine context. I think this context should be Dispatchers.Main by default.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions