Sorry for asking such trivial questions :wink: but...
# ballast
r
Sorry for asking such trivial questions 😉 but I'm a bit lost. I'm trying to implement a simple todomvc app. I've created a contract and a viewModel for the application and now I'm trying to add routing/navigation, which should support just three routes:
/
,
/active
, and
/completed
.
The routing is another viewModel
How should I connect the two viewmodels ?
I've also got a problem. In this line:
Copy code
val routerState: Backstack<TodoRoute> by router.observeStates().collectAsState()
collectAsState()
is not found
c
It’s not a problem at all, I’m happy to answer any/all questions you have! In general, it will help to think of ViewModels as related to a single screen, rather than the full application. So when a Route gets loaded, that route builds the screen’s UI, and creates the ViewModel at the root of the UI. If you have state that should be shared between screens, it should be pushed back into a “repository layer” or help in some other more global mechanism like a local DB
And
collectAsState()
is a Compose API, an extension function on
StateFlow
.
observeAsState()
returns a
StateFlow
, which is the important bit, and you can collect the states from it however works best with your UI
r
Ok, this is clear.
Going back to the routing - in my case I've got only one screen, but still want the routing to work (and change the state).
c
Much of the documentation is using Compose for code snippets, but Ballast is intentionally not tied to Compose. I purposely built the Android example without compose, to show how it could be used with a more traditional MVC-style UI framework.
r
Shoudl I just observe the changes and dispatch inputs?
c
There’s a couple ways to go about this. The first, and probably easiest to implement, is to just treat each route as a different screen (though you can absolutely share the same UI/VM for all 3). Then, when the route changes, it basically recreates the screen with the new values for you. In this case, the Router lives above your screen’s UI, and is the preferred way to do it. But you could have the Router basically live at the same level as the UI ViewModel. In this case, yes, you’d basically want to observe the state and on its changes, send an Input to the ToDoViewModel
r
what is a backstack ?
a list of routes?
c
The second way is definitely not a “wrong” way to do it, btw. I’m working on rewriting the IntelliJ plugin, and this is the route I’m going with that, since it just makes more sense for how the debugger tool window is build. Here’s the setup I’ve got here, which is along the lines of what you’re thinking
And yes, the Backstack is just a list of routes, after they’ve been matched to destination URLs.
So the
Route
is the matcher, and a
Destination
is a URL that is matched to a
Route
r
the current route is the first on this list?
c
It’s the last
r
Still don't know how to do this 🙂
Copy code
router.observeStates().onEach {
            val destination = it.backstack.lastOrNull()
            // I've got destination but I want route :)
        }.launchIn(scope)
It looks better:
Copy code
router.observeStates().onEach {
            it.backstack.renderCurrentDestination(
                route = { route: TodoRoute ->
                    when(route) {
                        TodoRoute.All -> TODO()
                        TodoRoute.Active -> TODO()
                        TodoRoute.Completed -> TODO()
                    }
                },
                notFound = { },
            )
        }.launchIn(scope)
Is this ok?
c
Yup, that’s looking good. And from there, you can have that snippet post an input back to the screen’s TODO VM:
Copy code
val router: Router<TodoRoute> = ...
val vm: BallastViewModel<TodoContract.Inputs, TodoContract.Events, TodoContract.State> = ...

router.observeStates().onEach {
    it.backstack.renderCurrentDestination(
        route = { route: TodoRoute ->
            when(route) {
                TodoRoute.All -> { vm.trySend(TodoContract.Inputs.ShowAll) } 
                TodoRoute.Active -> { vm.trySend(TodoContract.Inputs.ShowActive) } 
                TodoRoute.Completed -> { vm.trySend(TodoContract.Inputs.ShowCompleted) } 
            }
        },
        notFound = { },
    )
}.launchIn(scope)

vm.observeStates().onEach { todoState -> 
    updateUi(todoState)
}.launchIn(scope)
r
Thank you. I've got my app working 😉
A couple of issues
[TodoScreen] Input Queued: [object Object]
this is how input is logged in the browser when routing is activated
other inputs are logged correctly e.g.
[TodoScreen] Input Queued: ToggleActive(index=1)
it's probably object's toString()
Perhaps I should use new data object
c
It’s going to log the
toString()
of an object, which is unfortunately terrible in JS for most standard classes. Data classes have an intellijgible
.toString()
generated by Kotlin, but for objects or normal classes, yeah, its just using the platform’s default representation for
.toString()
And yes, I was super exicted when I saw the proposal for data objects, for this exact reason
r
1.9 😞
c
yeah…. Your best bet for now is to manually override the
.toString()
r
Another issue - the routing is not working (an error is thrown) when running the app from the local directory without server
c
What’s the error message you’re getting?
r
unfortunately it's obfuscated
Uncaught TypeError: Mn().m5s_1 is undefined
c
Hmm, I’m not sure what’s going on with that. Is it the full production bundle that’s being loaded locally? I’m wondering if it’s only loading the module without the Kotlin stdlib dependency, for example
r
I'll try to build a non minimized version
the same file works when opened from the http server so it's the issue with file:// url only
c
That’s super weird. Ballast shouldn’t be doing anything strange with Kotlin/JS, so I’m not sure what the problem would be.
Are you using the browser address bar sync?
r
Yes I'm using
withBrowserHashRouter
Without minimization the error is a bit more clear
message has been deleted
Copy code
Uncaught TypeError: Companion_getInstance_0().m5s_1 is undefined
    applyOrigin file:///home/rjaros/git/kvision-examples/todomvc-ballast/build/distributions/main.bundle.js:38066
c
I think that’s from the
io.ktor:ktor-http
dependency, which I’m using to parse URLs in common code. I think I might see part of the problem, though I’m not quite sure how to address it. URLBuilder.applyOrigin refers to a private property in its companion object. I’m wondering if
originUrl
has not been initialized at the point
applyOrigin()
is called. So likely some kind of JS initialization order issue, maybe?
r
I think it would be great if you consider dropping ktor dependency for ballast-navigation. It's probably makes some things easier but the bundle size of the application is very large because of ktor.
c
Yeah, I definitely do want to drop the dependency, I just haven’t had time to yet. It isn’t the entire Ktor client, but it does still have much more in there than I need
I would like to find a MPP library that just does URI parsing in pure Kotlin code (which is why i went with ktor). I’d rather not actual/expect it, so that I can be sure the router will work on all platforms without needing to explicitly support each one
r
The navigation module enlarges my bundle size from 470KB to 650KB.
The original app with redux was 315KB, but without any coroutines or flows. So the ktor overhead is horrible 😉
c
ugh, that’s bigger than I thought it was. And yeah, looks like I neglected to notice that
ktor-http
also depends on
ktor-utils
, which is quite a large module. Ktor is licensed under Apache, so maybe I can just copy the couple of files I need for URL parsing…
r
A nice addition would be also regular expression matching for paths
Nice work with ballast and thanks for your help!
I'll be working on ballast module for KVision, so probably could have some more questions soon 😉
c
I did consider regex, but chose to leave it out, for several reasons: 1) it would complicate the Route parser. Short of actually parsing Regex, it would be difficult to find a syntax that wouldn’t potentially cause some valid regex to not parse into a route correctly 2) it complicates matching. The dynamic parts right now are discrete and easily compared to each other, especially for weighting. Regex throws a huge wrench into this 3) The routes are all front-end routes, so you’re not necessarily trying to match weird URL patterns that you don’t control. Regex only gives you license to create really weird routes that might not match as you expect as the application grows 4) You can perform validation on path/query params when rendering the UI, it doesn’t necessarily need to be validated within the matcher. This is the general philosophy on typed parameters as well, and why there is also no syntax for declaring a parameter to be an Int, Long, etc.
And thank you for the kind words, I’m happy to help! I was going to suggest that it would be nice to have a KVision example added to the Ballast repo as well, once you’ve gotten the hang of the library and figured out how it fits best into KVision