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.16k stars 84 forks source link

How to organize root component and it's children in a way their lifecyle will be managed automatically? #730

Closed aivanovski closed 2 months ago

aivanovski commented 2 months ago

Hi, Thank you for this library. The idea looks really cool and the code looks clean and nice. But compared to all other libraries, this library is really requires tons of effort. I know I'm not the only one who complaining about this.

Here is the behavior, that I'm trying to implement with Decompose:

Is there a way to implement this on Android? This behavior is actually "native" for Android when using the single Activity approach (screens are fragments inside this Activity).

Here is my current code. My idea is to create LifecycleRegistry() for each child component manually and move it to a desired state. The problem is child component doesn't go through onStop(), onDestory() when I navigate between the screens with navigation.push or navigation.pop.

class RootComponent(
    componentContext: ComponentContext
) : ComponentContext by componentContext {
    val navigation = StackNavigation<Screen>()
    val childStack = childStack(
        source = navigation,
        serializer = Screen.serializer(),
        initialStack = { listOf(Screen.Login) },
        childFactory = { screen, _ -> createChildComponent(screen) }
    )

    private fun createChildComponent(screen: Screen): ComponentContext {
        val childLifecyle = LifecycleRegistry()

        // subscribe to RootComponent.lifecycle and move childLifecycle to the corresponding state
        childLifecyle.attachToParent(parent = this.lifecycle)

        return when (screen) {
            is Screen.Login -> {
                LoginScreenComponent(
                    component = childContext(someKey(), childLifecyle)
                )
            }
            is Screen.List -> {
                FlowListScreenComponent(
                    component = childContext(someKey(), childLifecyle)
                )
            }
            is Screen.Detail -> {
                DetailsComponent(
                    component = childContext(someKey(), childLifecyle)
                )
            }
        }
    }

    private fun LifecycleRegistry.attachToParent(parent: Lifecycle) {
        val child = this
        val observer = object : Lifecycle.Callbacks {
            override fun onCreate() {
                child.onCreate()
            }
            override fun onStart() {
                child.onStart()
            }
            override fun onResume() {
                child.onResume()
            }
            override fun onPause() {
                child.onPause()
            }
            override fun onStop() {
                child.onStop()
            }
            override fun onDestroy() {
                parent.unsubscribe(this)
            }
        }
        parent.subscribe(observer)
    }
}
arkivanov commented 2 months ago

Thanks for the feedback!

You don't need to anything special, each child components gets its own ComponentContext, which provides APIs for Lifecycle, state preservation, instance retaining (aka ViewModel) and back button handling. You just need to follow the Child Stack doc, or the Quick Start doc.

TLDR: you need to pass the ComponentContext instance that you receive as the second argument in your childFactory function to your child components via their constructors.

class RootComponent(
    componentContext: ComponentContext
) : ComponentContext by componentContext {
    val navigation = StackNavigation<Screen>()
    val childStack = childStack(
        source = navigation,
        serializer = Screen.serializer(),
        initialStack = { listOf(Screen.Login) },
        childFactory = ::createChildComponent,
    )

    private fun createChildComponent(screen: Screen, ctx: ComponentContext): ComponentContext {
        // Pass ctx to the child component
    }
aivanovski commented 2 months ago

Thank you for the quick answer, now everything works as expected.