Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Android) Using coroutines when creating a child crashes the app if the component context is created using defaultComponentContext() #195

Closed
YektaDev opened this issue Sep 2, 2022 · 9 comments
Labels
question Further information is requested

Comments

@YektaDev
Copy link

YektaDev commented Sep 2, 2022

Hi. I just found out that in order to create a Decompose component context using the defaultComponentContext function, which uses lifecycle.EssentyLifecycleInterop.subscribe underneath, breaks the app (most likely because of its interaction with androidx.lifecycle), and the workaround is to either not use coroutines when creating a child or create the child in Dispatchers.Main. Otherwise the app crashes immediately with error logs similar to this (Replaced app package with com.company.app):

E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.company.app, PID: 25237
    java.lang.IllegalStateException: Method addObserver must be called on the main thread
        at androidx.lifecycle.LifecycleRegistry.enforceMainThreadIfNeeded(LifecycleRegistry.java:323)
        at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:178)
        at com.arkivanov.essenty.lifecycle.EssentyLifecycleInterop.subscribe(AndroidExt.kt:32)
        at com.arkivanov.decompose.lifecycle.MergedLifecycle.<init>(MergedLifecycle.kt:27)
        at com.arkivanov.decompose.lifecycle.MergedLifecycle.<init>(MergedLifecycle.kt:15)
        at com.arkivanov.decompose.router.stack.RouterEntryFactoryImpl.invoke(RouterEntryFactoryImpl.kt:26)
        at com.arkivanov.decompose.router.stack.RouterEntryFactory$DefaultImpls.invoke$default(RouterEntryFactory.kt:8)
        at com.arkivanov.decompose.router.stack.StackControllerImpl.navigate(StackControllerImpl.kt:39)
        at com.arkivanov.decompose.router.stack.ChildStackController.navigateActual(ChildStackController.kt:45)
        at com.arkivanov.decompose.router.stack.ChildStackController.access$navigateActual(ChildStackController.kt:14)
        at com.arkivanov.decompose.router.stack.ChildStackController$queue$1.invoke(ChildStackController.kt:26)
        at com.arkivanov.decompose.router.stack.ChildStackController$queue$1.invoke(ChildStackController.kt:26)
        at com.arkivanov.decompose.SerializedQueue.drain(SerializedQueue.kt:23)
        at com.arkivanov.decompose.SerializedQueue.offer(SerializedQueue.kt:17)
        at com.arkivanov.decompose.router.stack.ChildStackController$eventObserver$1.invoke(ChildStackController.kt:38)
        at com.arkivanov.decompose.router.stack.ChildStackController$eventObserver$1.invoke(ChildStackController.kt:38)
        at com.arkivanov.decompose.Relay$queue$1.invoke(Relay.kt:13)
        at com.arkivanov.decompose.Relay$queue$1.invoke(Relay.kt:12)
        at com.arkivanov.decompose.SerializedQueue.drain(SerializedQueue.kt:23)
        at com.arkivanov.decompose.SerializedQueue.offer(SerializedQueue.kt:17)
        at com.arkivanov.decompose.Relay.accept(Relay.kt:25)
        at com.arkivanov.decompose.router.stack.StackNavigationImpl.navigate(StackNavigationFactory.kt:18)
        at com.arkivanov.decompose.router.stack.StackNavigatorExtKt.push(StackNavigatorExt.kt:16)
        at com.arkivanov.decompose.router.stack.StackNavigatorExtKt.push$default(StackNavigatorExt.kt:15)
        at com.company.app.root.RootComponentImpl$createChild$createAuthScreen$1.invoke-bFv73x8(RootComponentImpl.kt:42)
        at com.company.app.root.RootComponentImpl$createChild$createAuthScreen$1.invoke(RootComponentImpl.kt:42)
        at com.company.app.authentication.AuthenticationScreenImpl$1.invokeSuspend(AuthenticationScreenImpl.kt:51)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
    	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@1089d7d, Dispatchers.Default]

MainActivity.kt:

import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import com.arkivanov.decompose.defaultComponentContext
// ...
import android.os.Bundle

internal val app = MyCustomApplicationStarterLogic()

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        app.init(AppInitData(this, defaultComponentContext()))
        setContent { ApplicationScreen(app.root) }
    }
}

Emulator: Pixel 4 XL API 25 (Android 7.1.1 Google APIs | x86)

Just wanted to report this (edge?) case.

@arkivanov
Copy link
Owner

arkivanov commented Sep 3, 2022

The stack trace suggests that the navigation is performed on a background thread. The navigation must be always performed on the main thread. But without a reproducer code it's hard to say where is the mistake exactly.

@arkivanov arkivanov added the question Further information is requested label Sep 3, 2022
@YektaDev
Copy link
Author

YektaDev commented Sep 3, 2022

What I did that caused the issue was simply pushing a configuration to my stack navigation inside a coroutine. I know this can be solved simply by adding a withContext(Dispatchers.Main) {...} inside the coroutine right before the navigation, but I just wanted to report this behavior for the sake of it 😃.

@arkivanov
Copy link
Owner

Thanks for reporting. This is expected, we have to explicitly switch to the main despatcher before navigating.

@arkivanov arkivanov closed this as not planned Won't fix, can't repro, duplicate, stale Sep 3, 2022
@bennysway
Copy link

I have the same problem but in compose desktop. Logs are as follows

Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Access from different threads is detected, must be on the main thread only.Current thread: AWT-EventQueue-0. First thread: DefaultDispatcher-worker-1.
	at com.arkivanov.decompose.AssertMainThreadKt.assertMainThread(AssertMainThread.kt:8)
	at com.arkivanov.decompose.value.MutableValueImpl$special$$inlined$observable$1.afterChange(Delegates.kt:71)
	at kotlin.properties.ObservableProperty.setValue(ObservableProperty.kt:41)
	at com.arkivanov.decompose.value.MutableValueImpl.setValue(MutableValueBuilder.kt:21)
	at com.arkivanov.decompose.router.children.ChildrenFactoryKt.children$onAfterNavigate(ChildrenFactory.kt:106)
	at com.arkivanov.decompose.router.children.ChildrenFactoryKt.access$children$onAfterNavigate(ChildrenFactory.kt:1)
	at com.arkivanov.decompose.router.children.ChildrenFactoryKt$children$eventObserver$1.invoke(ChildrenFactory.kt:124)
	at com.arkivanov.decompose.router.children.ChildrenFactoryKt$children$eventObserver$1.invoke(ChildrenFactory.kt:119)
	at com.arkivanov.decompose.Relay$queue$1.invoke(Relay.kt:13)
	at com.arkivanov.decompose.Relay$queue$1.invoke(Relay.kt:12)
	at com.arkivanov.decompose.SerializedQueue.drain(SerializedQueue.kt:23)
	at com.arkivanov.decompose.SerializedQueue.offer(SerializedQueue.kt:17)
	at com.arkivanov.decompose.Relay.accept(Relay.kt:25)
	at com.arkivanov.decompose.router.stack.DefaultStackNavigation.navigate(DefaultStackNavigation.kt:11)
	at com.arkivanov.decompose.router.stack.StackNavigatorExtKt.bringToFront(StackNavigatorExt.kt:82)
	at com.arkivanov.decompose.router.stack.StackNavigatorExtKt.bringToFront$default(StackNavigatorExt.kt:81)
	at component.app.AppRootComponentImpl.gotoWindow(AppRootComponentImpl.kt:102)
	at component.app.AppRootComponentImpl$child$1.invoke(AppRootComponentImpl.kt:52)
	at component.app.AppRootComponentImpl$child$1.invoke(AppRootComponentImpl.kt:51)
	at component.login.root.LoginRootComponentImpl.openDashboardWindow(LoginRootComponentImpl.kt:151)
...
CommonButton(
            modifier = Modifier
                .width(418.dp)
                .height(56.dp)
                .align(Alignment.End),
            onClick = {
                component?.openDashboardWindow()
            },
            text = "Go to Dashboard",
...

@YektaDev
Copy link
Author

YektaDev commented Jan 5, 2023

@bennysway This check that you're encountering is actually added in a recent version, here.
Switching to Dispatchers.Main or Dispatchers.Main.immediate is required in this case.

@arkivanov
Copy link
Owner

arkivanov commented Jan 5, 2023

Yes, and also make sure that you create the root ComponentContext on the UI thread! See

Reopening the issue to mention this in the docs.

@arkivanov arkivanov reopened this Jan 5, 2023
@arkivanov
Copy link
Owner

Docs updated in 6b10c44.

@YektaDev
Copy link
Author

YektaDev commented Jan 7, 2023

Isn't the UI thread the thread in which the program executes by default before entering any coroutines, like in Android?

@arkivanov
Copy link
Owner

@YektaDev unfortunately no, the program starts on the main thread, and then Swing starts it's own UI thread. All event callbacks in Compose are called on the UI thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants