Me again! :grin: Ballast Navigation seems nice, b...
# ballast
m
Me again! 😁 Ballast Navigation seems nice, but it feels like it is designed for navigation triggered purely by UI actions. In particular, it is unclear what the intended pattern is for when the navigation is triggered in part by other work. For example: • User clicks a button • That triggers some I/O (database, Web service, whatever) • Where we navigate to is determined by the results of that I/O, and we cannot do that navigation until that I/O is done I can see having the UI layer be the bridge code: when the state from the I/O viewmodel says "I/O complete and here are the results", trigger navigation. That feels mildly icky. Is the vision that we graft Ballast Navigation onto an I/O-handling viewmodel, eschewing
withRouter()
and
BasicRouter
and instead building some sort of composite viewmodel? Or, is the vision that the I/O-handling viewmodel hold a reference to the router and tell it to navigate as needed? Or is there something else? Thanks!
s
For me it is the latter, the
EventHandler
holds a reference to a Router (f.e. I inject it using Koin). Here is the description on the Ballast website for events;
Copy code
Events are modeled similar to Inputs, but in the other direction; Events are sent from the ViewModel to be processed exactly once by the UI. This would typically be things like requests to navigate to another screen.
So in your example it would work like this in my head. Button gets pushed ->
.onClick
triggers a
postInput
->
Input
gets processed by InputHandler -> InputHandler triggers suspended I/O work in repo/service/etc... -> return value of I/O work gets interpreted and triggers corresponding
postEvent
->
Event
gets processed in EventHandler and f.e. calls
router.trySend(RouterContract.Inputs.GoToDestination(...))
thank you color 1
c
Yup, that’s right. Even though Ballast Navigation uses a “View Model”, the router object is best kept as a singleton and treated more like a Data Source in your app, similar to how you’d manage SharedPreferences or a local DB, rather than a UI VM. The root of your app’s UI then observes that router to handle the actual routing, but because the Router is a singleton it can be injected into any app component you need. For example, it can be injected into the EventHandler to perform navigation in response to UI events (you can also use it from the InputHandler to reduce boilerplate, but that couples your VM logic more closely to the Router). But it can also be injected into your Domain layer to perform navigation as the result of an API call, if needed.
m
> router object is best kept as a singleton I can see a top-level screen router being a singleton. I also have routers per screen for internal navigation (e.g., sub-tabs). For that, I am trying having the router be scoped to the screen's viewmodel, something like:
Copy code
class MyScreenViewModel(
    coroutineScope: CoroutineScope,
    reducer: MyScreenContract.Reducer,
    val myScreenRouter: MyScreenRouter = MyScreenRouter(coroutineScope)
) :
    BasicViewModel<MyScreenContract.Input, MyScreenContract.Event, MyScreenContract.State>(
        config = BallastViewModelConfiguration.Builder()
            .withLogging()
            .apply { inputStrategy = FifoInputStrategy() }
            .withViewModel(
                initialState = MyScreenContract.State(),
                inputHandler = reducer,
                name = "MyScreenViewModel"
            )
            .build(),
        eventHandler = MyScreenContract.EventProcessor(myScreenRouter),
        coroutineScope = coroutineScope
    )
My screen composable can access the router via the viewmodel property, and I can supply it to my
EventProcessor
as well. This allows the whole thing to be torn down when leaving the screen. Thank you both for the suggestions!
c
Most routing libraries do suggest using sub-routers for each tab, but I think that just makes things more confusing than helpful. Consider the main inspiration for Ballast Navigation: a browser’s URL bar. Web apps have gotten by quite well for a long time using just that one navigator and not trying to be clever with sub-routers and such. Tabbed navigation isn’t inherently different from a singleton router. The latest entry in the backstack is the current screen, and you just need some way to determine which tab is selected. To that end, you could easily add some metadata to your routes (with Route Annotations ) to determine which tab should be selected for any given route, for example. Or get clever with inspecting the backstack to see which of the tab was most recently viewed, highlighting the appropriate tab.
And to put it in more technical terms, sub-routers introduce implicit state that becomes difficult to manage. A user cannot get to a page within a tab without first visiting the tab itself to create the sub-router. The fact that one must first visit the top-level screen for a tab, create the sub-router object, then navigate within that sub-router to an inner page of the tab is a difficult workflow to reproduce if you want to allow deep-links to that page. The inner page is fully dependent on the top-level tab page. In contrast, with a singleton router, each page of your application is fully independent of any other page. You can deep-link to any page from any other page, and purely based on the backstack and metadata attached to the routes, you should be able to recreate any UI state you need. All state for your application is stored within the Router and can be managed from a single location, which itself is not dependent on any user actions or UI state and can be modified safely from anywhere in your app.