arkivanov / Decompose

Kotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing (navigation) and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.)
https://arkivanov.github.io/Decompose
Apache License 2.0
2.23k stars 84 forks source link

Multiple retainedComponent (RootComponent) ComponentContext for android app will crash #807

Open vishalbhimporwala opened 4 days ago

vishalbhimporwala commented 4 days ago

Here my RootComponent class

class RootComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
    private val navigation = StackNavigation<Configuration>()

    val childStack = childStack(
        key = "mainRoot",
        source = navigation,
        serializer = Configuration.serializer(),
        initialConfiguration = Configuration.SplashActivity,
        handleBackButton = true,
        childFactory = ::createRootChild
    )

    private fun createRootChild(
        configuration: Configuration, context: ComponentContext
    ): Child {
        return when (configuration) {
            is Configuration.SplashActivity -> Child.SplashActivity(
                SplashComponent(contextComponent = context, onTimerComplete = {
                    navigation.pushNew(Configuration.LoginActivity)
                })
            )

            is Configuration.LoginActivity -> Child.LoginActivity(
                LoginComponent(componentContext = context, onLogin = {
                    navigation.pushNew(Configuration.DashboardActivity)
                })
            )

            is Configuration.DashboardActivity -> Child.DashboardActivity(DashboardComponent(context))
        }
    }

    sealed class Child {
        data class SplashActivity(val component: SplashComponent) : Child()
        data class LoginActivity(val component: LoginComponent) : Child()
        data class DashboardActivity(val component: DashboardComponent) : Child()
    }

    @Serializable
    sealed class Configuration {
        @Serializable
        data object SplashActivity : Configuration()

        @Serializable
        data object LoginActivity : Configuration()

        @Serializable
        data object DashboardActivity : Configuration()
    }
}

Here my DashBoardRootComponent class

class DashBoardRootComponent(componentContext: ComponentContext) :
    ComponentContext by componentContext {
    private val navigation = StackNavigation<Configuration>()

    val childStack = childStack(
        key = "dashBoardRoot",
        source = navigation,
        serializer = Configuration.serializer(),
        initialConfiguration = Configuration.PlayListActivity,
        handleBackButton = true,
        childFactory = ::createDashBoardChild
    )

    private fun createDashBoardChild(
        configuration: Configuration, context: ComponentContext
    ): Child {
        return when (configuration) {
            is Configuration.PlayListActivity -> Child.PlayListActivity(
                PlayListComponent(componentContext = context, onPlayListClick = {
                    navigation.pushNew(Configuration.TrackListActivity)
                })
            )

            is Configuration.TrackListActivity -> Child.TrackListActivity(TrackListComponent(componentContext = context,
                onTrackListClick = {
                },
                onBackPress = {
                    navigation.pop()
                }))
        }
    }

    sealed class Child {
        data class PlayListActivity(val component: PlayListComponent) : Child()
        data class TrackListActivity(val component: TrackListComponent) : Child()
//        data class DashboardActivity(val component: DashboardComponent) : Child()
    }

    @kotlinx.serialization.Serializable
    sealed class Configuration {
        @kotlinx.serialization.Serializable
        data object PlayListActivity : Configuration()

        @kotlinx.serialization.Serializable
        data object TrackListActivity : Configuration()
    }

}

My App Component

@Composable
@Preview
fun App(root: RootComponent, dashBoardRoot: DashBoardRootComponent) {
    MaterialTheme {
        val childStack by root.childStack.subscribeAsState()
        Children(
            stack = childStack,
            animation = stackAnimation(slide()),
            content = { child ->
                when (val instance = child.instance) {
                    is RootComponent.Child.SplashActivity -> Splash(instance.component)
                    is RootComponent.Child.LoginActivity -> Login(instance.component)
                    is RootComponent.Child.DashboardActivity -> Dashboard(
                        instance.component,
                        dashBoardRootComponent = dashBoardRoot
                    )
                }
            }
        )
    }
}

For Android MainActivity 1st Try

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val rootF = retainedComponent {
            RootComponent(it)
        }
        val dashF = retainedComponent {
            DashBoardRootComponent(it)
        }
        setContent {
            App(rootF, dashF)
        }
    }
}

Logcat :

FATAL EXCEPTION: main
Process: com.neonmusic.app, PID: 3633
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.neonmusic.app/com.neonmusic.app.MainActivity}: java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3635)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:201)
    at android.os.Looper.loop(Looper.java:288)
    at android.app.ActivityThread.main(ActivityThread.java:7839)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
    at androidx.savedstate.SavedStateRegistry.registerSavedStateProvider(SavedStateRegistry.kt:110)
    at com.arkivanov.essenty.statekeeper.AndroidExtKt.StateKeeper(AndroidExt.kt:31)
    at com.arkivanov.essenty.statekeeper.AndroidExtKt.stateKeeper(AndroidExt.kt:54)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent(RetainedComponent.kt:101)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent(RetainedComponent.kt:52)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent$default(RetainedComponent.kt:45)
    at com.neonmusic.app.MainActivity.onCreate(MainActivity.kt:20)
    at android.app.Activity.performCreate(Activity.java:8051)
    at android.app.Activity.performCreate(Activity.java:8031)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3608)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792) 
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) 
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210) 
    at android.os.Handler.dispatchMessage(Handler.java:106) 
    at android.os.Looper.loopOnce(Looper.java:201) 
    at android.os.Looper.loop(Looper.java:288) 
    at android.app.ActivityThread.main(ActivityThread.java:7839) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)  

For Android MainActivity 2nd Try from chatGpt reference

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val root = retainedComponentWithKey(key = "RootComponent_mainRoot") {
            RootComponent(it)
        }
        val dashBoardRoot = retainedComponentWithKey(key = "DashBoardRootComponent_dashBoardRoot") {
            DashBoardRootComponent(it)
        }

        setContent {
            App(root.first, dashBoardRoot.first)
        }
    }

    private fun <T : Any> retainedComponentWithKey(
        key: String,
        factory: (ComponentContext) -> T
    ): Pair<T, String> {
        val component = retainedComponent(key = key, handleBackButton = true, factory = factory)
        return component to key
    }
}

Logcat :

FATAL EXCEPTION: main
Process: com.neonmusic.app, PID: 3248
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.neonmusic.app/com.neonmusic.app.MainActivity}: java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3635)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:201)
    at android.os.Looper.loop(Looper.java:288)
    at android.app.ActivityThread.main(ActivityThread.java:7839)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
    at androidx.savedstate.SavedStateRegistry.registerSavedStateProvider(SavedStateRegistry.kt:110)
    at com.arkivanov.essenty.statekeeper.AndroidExtKt.StateKeeper(AndroidExt.kt:31)
    at com.arkivanov.essenty.statekeeper.AndroidExtKt.stateKeeper(AndroidExt.kt:54)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent(RetainedComponent.kt:101)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent(RetainedComponent.kt:52)
    at com.arkivanov.decompose.RetainedComponentKt.retainedComponent$default(RetainedComponent.kt:45)
    at com.neonmusic.app.MainActivity.retainedComponentWithKey(MainActivity.kt:33)
    at com.neonmusic.app.MainActivity.onCreate(MainActivity.kt:20)
    at android.app.Activity.performCreate(Activity.java:8051)
    at android.app.Activity.performCreate(Activity.java:8031)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3608)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792) 
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) 
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210) 
    at android.os.Handler.dispatchMessage(Handler.java:106) 
    at android.os.Looper.loopOnce(Looper.java:201) 
    at android.os.Looper.loop(Looper.java:288) 
    at android.app.ActivityThread.main(ActivityThread.java:7839) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)  

I am facing this issue only for android

Do code for desktop also but it will working fine. Here Desktop entry point

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        title = "neon-music-kmp",
    ) {
        val root = remember { RootComponent(DefaultComponentContext(LifecycleRegistry())) }
        val dashBoardRoot = remember { DashBoardRootComponent(DefaultComponentContext(LifecycleRegistry())) }
        App(root, dashBoardRoot)
    }
}

Can any one explain me where i need to give unique key for android retainedComponent ? Tell me if i am wrong in anything.

arkivanov commented 4 days ago

Thanks for reporting! This looks like a bug. From the top of my head, retainedComponent was supposed to be the only one in an Activity. However, there is the key argument there, which probably there for a reason. I will try fixing it or provides updates here.

For now, please use the following approach.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val (root, dash) = retainedComponent {
            RootComponent(it.childContext("root")) to DashBoardRootComponent(it.childContext("dash"))
        }
        setContent {
            App(rootF, dashF)
        }
    }
}

Also regarding the desktop version, please make sure you are following the docs. Usually, components should be created outside of Compose.

vishalbhimporwala commented 4 days ago

Thank you so much sir,

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val (rootComponent, dashBoardRootComponent) = retainedComponent { RootComponent(it.childContext("root")) to DashBoardRootComponent(it.childContext("dash")) } setContent { App(rootComponent, dashBoardRootComponent) } }

}It will work as I need it to.

As per your suggestion I updated my desktop entry point code.

fun main() = application { Window( onCloseRequest = ::exitApplication, title = "neon-music-kmp", ) { val root = runOnUiThread { RootComponent( componentContext = DefaultComponentContext(LifecycleRegistry()) ) } val dashBoardRoot = runOnUiThread { DashBoardRootComponent( componentContext = DefaultComponentContext(LifecycleRegistry()) ) } App(root, dashBoardRoot) } }

Now it's working fine. Thanks again.

On Fri, Nov 8, 2024 at 3:57 PM Arkadii Ivanov @.***> wrote:

Thanks for reporting! This looks like a bug. From the top of my head, retainedComponent was supposed to be the only one in an Activity. However, there is the key argument there, which probably there for a reason. I will try fixing it or provides updates here.

For now, please use the following approach.

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    val (root, dash) = retainedComponent {
        RootComponent(it.childContext("root")) to DashBoardRootComponent(it.childContext("dash"))
    }
    setContent {
        App(rootF, dashF)
    }
}

}

Also regarding the desktop version, please make sure you are following the docs https://arkivanov.github.io/Decompose/component/overview/#jvmdesktop-with-compose. Usually, components should be created outside of Compose.

— Reply to this email directly, view it on GitHub https://github.com/arkivanov/Decompose/issues/807#issuecomment-2464347088, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHNIYGJ5TJBYFHJD7LYEBW3Z7SGX3AVCNFSM6AAAAABRMXEI6SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDINRUGM2DOMBYHA . You are receiving this because you authored the thread.Message ID: @.***>

-- - VISHAL BHIMPORWALA