Hosting a component in navigation-compose¶
This section may be useful when migrating from the official navigation-compose library to Decompose. We can convert screens to Decompose gradually (one by one), keeping the navigation untouched until every screen is converted. This section describes how we can host a Decompose component (or a tree of components) in a Composable
screen managed by navigation-compose
library.
Note
This section implies minimum Decompose version 3.0.0-alpha06
.
Warning
The navigation-compose
library provides only two scopes for a screen: the Composable
function of the scren and the ViewModel
scope. There is no such a scope that is also destroyed on configuration change. So the only scope where we can host Decompose components is the ViewModel
scope. This means we should take extra care to not leak any objects - don't pass NavController
(or any other objects with a narrower scope), don't pass lamdas or callbacks capturing those objects, etc.
Here is a function that allows hosting a Decompose component in a navigation-compose
screen.
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.ComponentContextFactory
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.essenty.backhandler.BackDispatcher
import com.arkivanov.essenty.backhandler.connectOnBackPressedCallback
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.create
import com.arkivanov.essenty.lifecycle.destroy
import com.arkivanov.essenty.lifecycle.pause
import com.arkivanov.essenty.lifecycle.resume
import com.arkivanov.essenty.lifecycle.start
import com.arkivanov.essenty.lifecycle.stop
import com.arkivanov.essenty.statekeeper.SerializableContainer
import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
import com.arkivanov.essenty.statekeeper.getSerializableContainer
import com.arkivanov.essenty.statekeeper.putSerializableContainer
@Composable
fun <T> rememberRetainedComponent(key: String = "ComposableComponent", factory: (ComponentContext) -> T): T {
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycle = lifecycleOwner.lifecycle
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val holder =
viewModel {
val handle = createSavedStateHandle()
val ctx = RetainedComponentContext(handle.get<Bundle>(key)?.getSerializableContainer(key))
handle.setSavedStateProvider(key) { Bundle().apply { putSerializableContainer(key, ctx.stateKeeper.save()) } }
Holder(factory(ctx), ctx)
}
DisposableEffect(lifecycle) {
val observer = LifecycleAdapter(holder.componentContext.lifecycle)
lifecycle.addObserver(observer)
onDispose { lifecycle.removeObserver(observer) }
}
if (onBackPressedDispatcher != null) {
DisposableEffect(lifecycleOwner, onBackPressedDispatcher) {
val onBackPressedCallback = holder.componentContext.onBackPressedCallback
onBackPressedDispatcher.addCallback(lifecycleOwner, onBackPressedCallback)
onDispose(onBackPressedCallback::remove)
}
}
return holder.instance
}
private class LifecycleAdapter(
private val lifecycle: LifecycleRegistry
) : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
lifecycle.create()
}
override fun onStart(owner: LifecycleOwner) {
lifecycle.start()
}
override fun onResume(owner: LifecycleOwner) {
lifecycle.resume()
}
override fun onPause(owner: LifecycleOwner) {
lifecycle.pause()
}
override fun onStop(owner: LifecycleOwner) {
lifecycle.stop()
}
}
private class RetainedComponentContext(savedState: SerializableContainer?) : ComponentContext {
override val lifecycle: LifecycleRegistry = LifecycleRegistry()
override val stateKeeper: StateKeeperDispatcher = StateKeeperDispatcher(savedState)
override val instanceKeeper: InstanceKeeperDispatcher = InstanceKeeperDispatcher()
override val backHandler: BackDispatcher = BackDispatcher()
val onBackPressedCallback: OnBackPressedCallback = backHandler.connectOnBackPressedCallback()
override val componentContextFactory: ComponentContextFactory<ComponentContext> =
ComponentContextFactory(::DefaultComponentContext)
}
private class Holder<out T>(
val instance: T,
val componentContext: RetainedComponentContext,
) : ViewModel(componentContext.lifecycle::destroy, componentContext.instanceKeeper::destroy)
Here is the usage example.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.arkivanov.decompose.ComponentContext
@Composable
fun App() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "home") {
composable("home") {
HomeScreen(onShowDetails = { nav.navigate("details") })
}
composable("details") {
val detailsComponent = rememberRetainedComponent(factory = ::DetailsComponent)
DetailsComponent(component = detailsComponent, onBack = nav::popBackStack)
}
}
}
@Composable
fun HomeScreen(onShowDetails: () -> Unit) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(onClick = onShowDetails) {
Text("Go to details")
}
}
}
class DetailsComponent(
componentContext: ComponentContext,
) : ComponentContext by componentContext {
// Some code here
}
@Composable
fun DetailsComponent(component: DetailsComponent, onBack: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Button(onClick = onBack) {
Text("Go back")
}
}
}