https://kotlinlang.org logo
Title
l

lazt omen

04/29/2022, 6:37 AM
Hi, I'm new to compose. I wanted to know if there is an existing opinionated frameworks established around of compose that provides a standard structure for things like routing, state management and internationalization.
c

Casey Brooks

04/29/2022, 2:53 PM
Routing: Decompose is one of the more commonly-referenced libraries for routing in desktop apps, but personally, I’ve found that desktop apps don’t benefit from those big, heavy routing frameworks like mobile apps do. I obviously don’t know how your desktop app is structured, but “routing” typically works on full-page or full-screen locations, but desktop apps usually only have a couple of actual “screens” or windows and do more with swapping out components like editor tabs, which doesn’t work as well with those traditional routing frameworks. I would personally recommend starting without any routing library, just doing it manually, and see if you actually need this • State Management: I’ve been building just that. Ballast is an MVI library which aims to add an opinionated structure to Kotlin applications. Plus, it was initially built for Compose Desktop, unlike most MVI libraries that are very Android-focused, so you can be sure it will have great support for Desktop apps, and I just released it to a stable 1.0.0 last week! • I18n: I haven’t really found anything good here, unfortunately, but I really haven’t looked too hard. I don’t think Java/Swing really offers anything out-of-the-box for i18n like Android does, so you’re kind-of on your own for making sure your UI strings are actually localized
l

lazt omen

04/29/2022, 2:59 PM
Thanks a lot. I was already taking a look at ballast and it look very promising. It's a shame there isn't a full fledge framework like this but maybe is not necessary after all and I'm still locked inmy web dev mindset.
c

Casey Brooks

04/29/2022, 3:05 PM
Yeah, I would like to see that too. The desktop app I've been working on I've tried to build in that manner, having an opinionated application structure/hierarchy (including routing, DI and component lifecycles), but it's definitely not something I can commit to putting any serious resources into at the moment.
a

Arkadii Ivanov

04/29/2022, 9:02 PM
The todoapp example showcases both Decompose and MVIKotlin libs -. https://github.com/JetBrains/compose-jb/tree/master/examples/todoapp. You can also play with the time travel debugger - https://twitter.com/arkann1985/status/1357447358192226306
👀 1
l

lazt omen

04/30/2022, 3:35 PM
I'm Trying to make a simple router using ballast.
object RouterContract {
    data class State(
        val currentPage: PageRoute
    )

    sealed class Inputs {
        data class Navigate(val newRoute: PageRoute) : Inputs()
    }

    sealed class Events {
    }
}

class RouterInputHandler : InputHandler<RouterContract.Inputs, RouterContract.Events, RouterContract.State> {
    override suspend fun InputHandlerScope<RouterContract.Inputs, RouterContract.Events, RouterContract.State>.handleInput(
        input: RouterContract.Inputs
    ) = when (input) {
        is RouterContract.Inputs.Navigate -> {
            updateState {
                it.copy(
                    currentPage = input.newRoute
                )
            }
        }
    }
I have no idea what I'm supposed to do next. I want to be able to access this instance in all my component tree so I can change the current route.
c

Casey Brooks

04/30/2022, 4:04 PM
Here’s the workflow description in the docs for a more in-depth guide, but generally-speaking, the workflow looks like this: 1) Define a Contract 2) Write the InputHandler 3) Write the EventHandler 4) Combine everything into a ViewModel 5) Inject the ViewModel to your UI and start using it So in your case, since you don’t need any Events, your next step would be creating the ViewModel. For Compose Desktop, that would mean using
BasicViewModel
as the base class, and there’s an
eventHandler { }
helper function for stubbing out the required
EventHandler
parameter of it.
class RouterViewModel(
    viewModelCoroutineScope: CoroutineScope
) : BasicViewModel<
    RouterContract.Inputs,
    RouterContract.Events,
    RouterContract.State>(
    config = BallastViewModelConfiguration.Builder()
        .forViewModel(
            initialState = RouterContract.State(),
            inputHandler = RouterInputHandler(),
            name = "Router",
        ),
    eventHandler = eventHandler { },
    coroutineScope = viewModelCoroutineScope,
)
From here, you need to create an instance of the ViewModel and observe it in the UI, as shown in the Ballast Compose Desktop example
Also, there is a fair amount of boilerplate involved with all of this, but I just added a bunch of templates in version 1.1.0 of the Intellij plugin, to quickly scaffold out all these classes for you. This version is still pending review and should be approved in the next day or so, or you can download the zip and install it to Intellij manually to use right now
For accessing the router anywhere in your application, it’s more up to how to want to use the router. You could pass it through CompositionLocals and do navigation directly within the Composable callbacks. This would be the “quick and dirty” route, but ultimately give you a bit less control over the navigation in general and ties you more directly to the specific Router library you’re using. If you’re using Ballast everywhere, it would be better to use Events from any given screen/component for handling “navigation”, and then injecting the Router into the EventHandler to actually perform the navigation request. This keeps a nice separation between your main business logic and the router, making it easier to swap out for something else in the future if you need to, and gives you a bit more control over which screens are able to navigate somewhere else.
l

lazt omen

04/30/2022, 4:54 PM
How do I call my inputs and events inside my component? Should I pass the view model down through composition local?
Maybe I'm looking at this the wrong way. But my idea was to just provide the router object to my entire application to be able to trigger the route change from any component. I'm not using any routing solution just a custom object.
c

Casey Brooks

04/30/2022, 8:01 PM
You can if you’d like, but the better way is to do it the normal Compose way of “lifting state”, in this case to the root of the screen. So you’d pass the State and a
postInput
callback through your composition tree, and only at the root do the Composable functions actually get tied to the ViewModel. They’re just normal callbacks at that point. The source for the ScoreKeeper example should help you understand how it works. If you passed the whole ViewModel through CompositionLocals, the idea is the same that the ViewModel lives at the root composable for the screen, and it’s just accessed implicitly rather than explicitly. For the actual navigation, you’d create Inputs for each navigation destination, and then those Inputs fire off the Events that actually perform the navigation. Give me a minute to throw together a quick example for you
l

lazt omen

04/30/2022, 8:37 PM
Passing props deep down the trees stroke me ass odd at first. I felt it could become unmaintainable pretty fast. Now I see that if you only have to pass state and postInput to all your components its pretty easy to maintain them. Thanks. Should I also pass a callback for postEvent?
Actually I don't see how to call events. Are events supposed to be side effects?
c

Casey Brooks

04/30/2022, 8:41 PM
Here’s a gist demonstrating the basic concepts, hopefully it’s helpful. I’ve omitted many of the details, such as the actual Ballast classes needed, so that the main use-cases are made more apparent. The example using DI is the way I typically handle this kind of situation, but all 3 are perfectly valid choices, so you can use the one that makes the most sense and looks the cleanest to you. Passing individual props, or callbacks for each Input type does get very cumbersome, that’s why it’s best to pass the whole
State
object around, and the callback is just for the generic Inputs superclass, even it only 1 input type if sent from a deeper Composable function. And for a
postEvent
callback, no, you don’t need that. You can’t post an Event directly to the ViewModel from “outside”, it can only be sent as part of handling an input.
👀 1
l

lazt omen

04/30/2022, 9:25 PM
Finally. This feels like a pretty good approach to routing in my app. I ended up going the CompositionLocal route. Thank you very much for all your help.
c

Casey Brooks

05/01/2022, 1:40 AM
Glad to hear it, I'm happy to help!