How to do navigation in a master/detail kind of UI? #47
-
So as I have been playing around with Decompose, its a pretty great framework for abstracting out routing into the common module. I have just noticed in the Todo Example, that when a new component is routed to that the entire screen is replaced. One common practice in building UI's for larger screens is the master/detail layout like shown below: So I was curious how something like this could be achieved in Decompose? I could see this being useful in other types of layouts as well if a navigation drawer or bottom nav were used, for example clicking on a different tab in a bottom nav shouldn't necessarily reload the whole bottom nav. The readme mentions :
Which if my understanding is correct, the router only has a couple methods being push and pop and if it were to be like a fragment manager might also need a replace method to achieve something like this? I'm guessing that this might also need some nested routers potentially to achieve something like this? interface Router<C : Parcelable, out T : Any> {
val state: Value<RouterState<C, T>>
fun push(configuration: C)
fun pop()
} |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 6 replies
-
Good question! Do you need to support both kinds of navigation (depending on mobile/desktop build type), or just one of them? |
Beta Was this translation helpful? Give feedback.
-
I will provide solutions for both. So first of all let's decompose it. We can create separate components for Items data class Item(
val id: String,
val text: String
)
interface ItemsComponent {
val models: Value<Model>
fun onItemClicked(id: String)
data class Model(
val items: List<Item>
)
sealed class Output {
data class ItemSelected(val id: String) : Output()
}
}
class ItemsComponentImpl(
private val output: (ItemsComponent.Output) -> Unit
) : ItemsComponent {
override val models: Value<ItemsComponent.Model> =
MutableValue(
ItemsComponent.Model(
items = listOf(
Item(id = "id1", text = "Item 1"),
Item(id = "id2", text = "Item 2"),
Item(id = "id3", text = "Item 3")
)
)
)
override fun onItemClicked(id: String) {
output(ItemsComponent.Output.ItemSelected(id = id))
}
}
@Composable
fun Items(component: ItemsComponent) {
TODO()
} Details interface DetailsComponent {
val models: Value<Model>
fun onCloseClicked()
data class Model(
val title: String,
val text: String
)
sealed class Output {
object Finished : Output()
}
}
class DetailsComponentImpl(
itemId: String,
private val output: (DetailsComponent.Output) -> Unit
) : DetailsComponent {
override val models: Value<DetailsComponent.Model> =
MutableValue(
DetailsComponent.Model(
title = "Item $itemId title",
text = "Item $itemId text"
)
)
override fun onCloseClicked() {
output(DetailsComponent.Output.Finished)
}
}
@Composable
fun Details(component: DetailsComponent) {
TODO()
} And now we can aggregate them in a parent component. We can create separate components for multi-pane and single-pane modes. MultiPane interface MultiPaneComponent {
val items: ItemsComponent
val routerState: Value<RouterState<*, Child>>
sealed class Child {
object None : Child()
data class Details(val component: DetailsComponent) : Child()
}
}
class MultiPaneComponentImpl(
context: ComponentContext
) : MultiPaneComponent, ComponentContext by context {
private val router =
router<Configuration, MultiPaneComponent.Child>(
initialConfiguration = Configuration.None,
handleBackButton = true,
componentFactory = ::child
)
override val items: ItemsComponent = ItemsComponentImpl(output = ::onItemsOutput)
override val routerState: Value<RouterState<*, MultiPaneComponent.Child>> = router.state
private fun child(configuration: Configuration, context: ComponentContext): MultiPaneComponent.Child =
when (configuration) {
is Configuration.None -> MultiPaneComponent.Child.None
is Configuration.Details ->
MultiPaneComponent.Child.Details(
DetailsComponentImpl(
itemId = configuration.id,
output = ::onItemDetailsOutput
)
)
}
private fun onItemsOutput(output: ItemsComponent.Output): Unit =
when (output) {
is ItemsComponent.Output.ItemSelected -> router.push(Configuration.Details(id = output.id))
}
private fun onItemDetailsOutput(output: DetailsComponent.Output): Unit =
when (output) {
DetailsComponent.Output.Finished -> TODO()
}
private sealed class Configuration : Parcelable {
@Parcelize
object None : Configuration()
@Parcelize
data class Details(val id: String) : Configuration()
}
}
@Composable
fun MultiPane(component: MultiPaneComponent) {
Row {
Items(component.items)
Children(component.routerState) { child, _ ->
when (child) {
is MultiPaneComponent.Child.None -> Unit
is MultiPaneComponent.Child.Details -> Details(child.component)
}.let {}
}
}
} SinglePane interface SinglePaneComponent {
val routerState: Value<RouterState<*, Child>>
sealed class Child {
data class Items(val component: ItemsComponent) : Child()
data class Details(val component: DetailsComponent) : Child()
}
}
class SinglePaneComponentImpl(
context: ComponentContext
) : SinglePaneComponent, ComponentContext by context {
private val router =
router<Configuration, SinglePaneComponent.Child>(
initialConfiguration = Configuration.Items,
handleBackButton = true,
componentFactory = ::child
)
override val routerState: Value<RouterState<*, SinglePaneComponent.Child>> = router.state
private fun child(configuration: Configuration, context: ComponentContext): SinglePaneComponent.Child =
when (configuration) {
is Configuration.Items ->
SinglePaneComponent.Child.Items(
ItemsComponentImpl(
output = ::onItemsOutput
)
)
is Configuration.Details ->
SinglePaneComponent.Child.Details(
DetailsComponentImpl(
itemId = configuration.id,
output = ::onItemDetailsOutput
)
)
}
private fun onItemsOutput(output: ItemsComponent.Output): Unit =
when (output) {
is ItemsComponent.Output.ItemSelected -> router.push(Configuration.Details(id = output.id))
}
private fun onItemDetailsOutput(output: DetailsComponent.Output): Unit =
when (output) {
DetailsComponent.Output.Finished -> router.pop()
}
private sealed class Configuration : Parcelable {
@Parcelize
object Items : Configuration()
@Parcelize
data class Details(val id: String) : Configuration()
}
}
@Composable
fun SinglePane(component: SinglePaneComponent) {
Children(routerState = component.routerState) { child, _ ->
when (child) {
is SinglePaneComponent.Child.Items -> Items(child.component)
is SinglePaneComponent.Child.Details -> Details(child.component)
}.let {}
}
} |
Beta Was this translation helpful? Give feedback.
I will provide solutions for both.
So first of all let's decompose it. We can create separate components for
Items
andDetails
parts.Items