Routing between screens elegantly? #56
-
I'm writing an application that shares logic between an Android and iOS application. I'm in the process of trying to port it to decompose. I'm finding a few instances where things require more boiler plate / are unexpectedly inelegant. Heres a simplified form of the class that I use for my backstack: sealed class Screen : Parcelable {
@Parcelize
object AppLoading : Screen()
// ...
} Here is the router defined in my private val router: Router<Screen, Any> = router(
initialConfiguration = Screen.AppLoading,
handleBackButton = true,
componentFactory = { screen, context ->
when (screen) {
is Screen.AppLoading -> AppLoadingComponent(context, repository).model
else -> TODO()
}
}
) and here is my RootComposable: @Composable
fun RootComponent.Model.RootComposable() {
Children(child) { model, screen ->
when (screen) {
is Screen.AppLoading -> (model as AppLoadingComponent.Model).AppLoadingComposable()
else -> TODO()
}
}
} Having to cast the model object for routing to get the Component's model to the view is pretty ugly. What is a better way that I can do this? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 6 replies
-
I will work off your initial example assuming this is what lives in your sealed class Screen : Parcelable {
@Parcelize
object AppLoading : Screen()
object Main : Screen()
// ...
} So normally in the sealed class Child {
object AppLoading : Child()
data class Main(
val mainComponent: MainComponent
) : Child()
} Your router would then need to add the child as the second parameter to the router type private val router: Router<Screen, RootComponent.Child> = router(
initialConfiguration = Screen.AppLoading,
handleBackButton = true,
componentFactory = { screen, context ->
when (screen) {
is Screen.AppLoading -> RootComponent.Child.AppLoading
is Screen.Main -> RootComponent.Child.Main(/*create your main component using context*/)
}
}
) Finally your root composable would look as follows: @Composable
fun RootComposable(component: RootComponent) {
Children(routerState = component.routerState, animation = crossfade()) { child, _ ->
when(child) {
is RootComponent.Child.AppLoading -> AppLoadingComposable()
is RootComponent.Child.Main -> MainComposable(child.mainComponent)
}
}
} Slightly different approach to what you were trying to do, but I think it should solve the problem you were having needing to cast the model object. |
Beta Was this translation helpful? Give feedback.
-
As @plusmobileapps pointed, there is not much we can do here, separation of concerns requires some boilerplate. There are two
The first case is required due to the nature of instantiation of child components. This is very similar to what we have with AndroidX The second case is due to the fact that you need iOS support and so pluggable UI (Compose, SwiftUI, etc.). It is possible to have the following API here (like Jetpack Compose Navigation style): Children(component.routerState) {
child<Child.Main> { TodoMainContent(it.component) }
child<Child.Edit> { TodoEditContent(it.component) }
} But it does not save much code (still the complexity is N). And on the other hand there is no compile time safety. If you add another child, the code will compile without covering the new case. If you would need only Compose UI, then it would be possible to use polymorphism here and avoid the second switch completely. This approach is used in my another article: Fully cross-platform Kotlin applications (almost). Overall, I think there is not so much boilerplate in total. The framework is quite concise and extendable. At the same time it is compile time safe, which is the priority at the moment. |
Beta Was this translation helpful? Give feedback.
I will work off your initial example assuming this is what lives in your
RootComponentImpl
with the addition of a main screen.So normally in the
RootComponent
I create a child class which will take the needed component as a parameter.Your router would then need to add the child as the second parameter to the router type