lazt omen
07/23/2022, 5:59 PMobject RouterContract {
data class State(
val currentRoute: PageRoute = PageRoute.SETTINGS,
val routeMetadata: Map<String, Any> = mapOf(),
val showWindow: Boolean = true,
val activeDialog: (@Composable () -> Unit)? = null
)
sealed class Inputs {
data class NavigateTo(
val newRoute: PageRoute,
val metadata: Map<String, Any> = mapOf()
) : Inputs()
object CloseApp : Inputs()
data class ShowDialog(val newDialog: @Composable () -> Unit) : Inputs()
object CloseDialog: Inputs()
}
sealed class Events
}
This is how I call the dialogs
if (routerState.activeDialog != null) {
Surface(
color = Color.White.copy(alpha = 0.6F),
modifier = Modifier.fillMaxSize()
.clickable(MutableInteractionSource(), indication = null) {
routerVm.trySend(RouterContract.Inputs.CloseDialog)
}
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
routerState.activeDialog?.invoke()
}
}
}
Casey Brooks
07/25/2022, 2:50 PMState
and postInput
is more for the screen-level ViewModels, in following with the principles of Compose of lifting screen state, and helps avoid tight coupling to the ViewModel itself. So it’s preferable to lift the Ballast state, but not strictly necessary.
Also, the problem here isn’t really a Ballast problem, but more general Compose. I’ve run into difficulties passing Composable lambdas up the tree because it kinda breaks the mental model and makes it difficult to update properly. You’ll basically need to observe the VM state directly within that lambda, so passing the VM itself will be necessary to update the dialog contents as you’d expect. You’ll need to think of that lambda as a new content root with nothing in it, because thats where it’s actually being placed in the composition (the root of your application with the router), despite the fact that the lambda is created further down the tree, which is really unintuitive.RouterContract.State
, but maybe take an approach more similar to the route itself and just place some “token” representing the active dialog in the router. Then your screens can observe the Router state in some way, check if the token is one they care about, and render the dialog deeper within the composition rather than at the root.
There are a couple ways you could implement this system, depending on how complex you want to get, where you want to post dialogs from, and how tightly you want to couple yourself to the Router.
1) Just pass the Router state/postInput into your screens (or the VM itself), along with the screen’s VM state. Could potentially use a CompositionLocal or DI to hold onto the Router ViewModel to keep things tidier. This would be the simplest method, but couples your screens pretty tightly to the Router.
2) Treat the Router as a dependency of each screen’s own ViewModels. Expose the methods for showing/hiding dialogs on the individual screen contracts, and it’s only on the implementation of the InputHandler that you’re actually interacting with the Router. This can help you build your screens decoupled from the router itself and clarify which dialogs belong to which screens, at the cost of some additional boilerplateobject SettingsDialog : DialogRoute()
object SettingsContract {
data class State(
val showDialog: Boolean = false,
)
sealed class Inputs {
object Initialize : Inputs()
data class RouterStateUpdated(val routerState: RouterContract.State) : Inputs()
data class ShowDialog(val newDialog: DialogRoute) : Inputs()
object CloseDialog : Inputs()
}
sealed class Events
}
class SettingsInputHandler(
private val router: RouterViewModel
) : InputHandler<
SettingsContract.Inputs,
SettingsContract.Events,
SettingsContract.State> {
override suspend fun InputHandlerScope<
SettingsContract.Inputs,
SettingsContract.Events,
SettingsContract.State>.handleInput(
input: SettingsContract.Inputs
) = when (input) {
is SettingsContract.Inputs.Initialize -> {
observeFlows(
"router",
router.observeStates().map { SettingsContract.Inputs.RouterStateUpdated(it) }
)
}
is SettingsContract.Inputs.RouterStateUpdated -> {
updateState { it.copy(showDialog = input.routerState.activeDialog is SettingsDialog) }
}
is SettingsContract.Inputs.ShowDialog -> {
router.trySend(RouterContract.Inputs.ShowDialog(SettingsDialog))
noOp()
}
is SettingsContract.Inputs.CloseDialog -> {
router.trySend(RouterContract.Inputs.CloseDialog)
noOp()
}
}
}