Wouldnt it be beneficial to have navigation logic ...
# compose
z
Wouldnt it be beneficial to have navigation logic outside of compose? Ive seen quite a few navigation libraries where a navigator sits at the root, and the different screen composables of the app delegate onViewDetails, etc, to the navigator which manipulates the backstack to show the appropiate screen. 🧵
đź‘Ť 1
đź’ˇ 1
Instead, the navigator could sit outside of compose and expose its backstack for compose to observe. That way, viewmodels can interact with it safely; and voila, youre no longer mixing business logic with view logic. Preserving the navigation state comes for free with the help of including it in compose, but you can easily set that up outside of compose as well.
c
You can fairly easily do this. What we do at the company I am working at, is we inject a class called Navigator into every ViewModel that is associated with a screen. The same instance is injected at the top level of the app, where the compose navigation is also added. We are passing through this object instructions like NavigateTo (Specific screen), or PopUp (One screen or until a specific screen) You can implement these instructions as sealed classes or Enum values or different event Flow-s whatever works for you.
c
Funny you should mention that, I actually just started working on adding exactly this to my state management library, #ballast. It’s still in it’s very early stages on the v2 branch, and the repo is currently blocked on JS, but is functional enough to demonstrate its usefulness on other platforms, I think. I would definitely agree that it’s a lot easier to reason about and to make complicated updates to the backstack when it’s not tied to the view at all, but is just “data” that is observed in Compose like anything else. The things that become more difficult are managing transition animations between states, and applying any kind of view-related lifecycle to the states. But if you’re using Compose, you probably don’t care about that.
j
we also do this. our navigator has sat above UI and presenters for many years to great effect. it's why migrating to compose UI for UI and Molecule for presenters piecemeal has been so easy.
c
This is also basically how browser routing has always worked. The browser itself is the thing that manages the backstack, and you just render the appropriate page when it lands on a particular URL. So this idea is already very prevalent, just not so much on native mobile platforms (yet)
c
There are some caveats around doing this if you use navigation-compose though, specifically, if you use a Navigator abstraction that is injected into the
ViewModel
and that the implementation of Navigator abstraction in turn uses
rememberNavController()
, The problem is the NavController returned by rememberNavController contains activity-scoped dependencies. So it is not safe to constructor-inject into a ViewModel. Instead, you need to use your DI library's "injection-with-parameters" feature to pass in the navController as a parameter to every ViewModel, and then use it to obtain your Navigator implementation that uses NavController.
j
Jetpack libraries making architectural improvements fraught with foot guns? Who could have seen that coming?!?
🤣 3
c
@curioustechizen I think that’s the whole point though: navigation-compose is tied directly to the view, which makes it unnecessarily clunky and difficult to extend beyond its explicitly-stated goals. But maybe we don’t need to be using that library at all, since it is so easy to create your own simple “router”
z
Well, I guess we all agree ❤️ Im just trying to understand why there are so many compose specific navigation libraries popping up right now - when separated approaches are so much better (in every way, imo).
c
My guess is that everyone’s used to Androidx Navigation, which got extended to Compose with a view-specific version. And all these other libraries are trying to copy that.
s
@jw Is navigation handled in workflows or sth else?
j
Register uses workflow. Cash has its own thing.
đź‘Ť 1
a
This is a super interesting thread. Do we have any good articles / code samples demonstrating such an implementation?
c
Here’s the most basic example of what this might look like:
Copy code
class RouterViewModel(private val initialRoute: String) {
    private val _backstack = MutableStateFlow(listOf(initialRoute))
    public val backstack get() = _backstack.asStateFlow()

    fun navigateTo(route: String) {
        _backstack.update { it + route }
    }

    fun goBack() {
        _backstack.update { it.dropLast(1) }
    }

    fun replaceTop(route: String) {
        _backstack.update { it.dropLast(1) + route }
    }
}

@Composable
fun App() {
    val router = remember { RouterViewModel("/") }
    val backstack by router.backstack.collectAsState()
    val currentScreen by derivedStateOf { backstack.last() }

    when(currentScreen) {
        "/" -> HomeScreen()
        "/settings" -> SettingsScreen()
        "/account" -> AccountScreen()
    }
}
The Router is just a normal class and can be passed around anywhere, so anything is free to update the backstack as it needs.
đź’ˇ 1
This example works purely with hard-coded paths, but you can imagine it being expanded such that the paths in the backstack are parsed as URIs, and matched up to route patterns like
/posts/:postId
, and you use the route patterns in the
when
block instead of specific URIs This is the basic idea that I’ve started building with my Ballast library, though it’s nowhere near ready yet and has no documentation. But it’s all built on top of the solid and extensible Ballast MVI core library, and will benefit from things like automatic logging and time-travel debugging
đź‘Ś 1
a
I think even with Androidx navigation this is the recommended way as shown here.
s
This is probably the opposite of what OP said, but I'd like to show my navigation alternative. I wanted to have a simple,
ViewModel
-free and composable-like navigation system. I also wanted the possibility to pass complex data or callbacks. This is how the code looks like:
Copy code
@Composable
fun HomeScreen() {
    val accountRoute: Route1<String> = rememberAccountRoute()

    // it handles transition, back press etc
    RouteHandler {
        accountRoute { accountId: String ->
            AccountScreen(
                accountId = accountId,
                onDidStuff = {
                     // I can have callbacks!
                }
            )
        }

        // main content
        host {
            var accountId: String by modelOrWhatever.collectAsState()

            BasicText(
                text = "Account",
                modifier = Modifier
                    // go to account
                    .clickable { accountRoute(accountId) }
            )
        }
    }
}
It is more verbose because of its explicitness, but I'm satisfied with the visual look of it
z
Im using a setup similar to workflow, where my root workflow has a backstack in its state that gets updated in response to child actions. Each destination/route has the inputs required to render that particular screen, and the entire backstack is wrapped inside another screen which makes handling animations between them a breeze (in compose). As an added bonus, all the navigation related logic is multiplatform compatible and ridiculously simple. Its been mentioned already, but I can highly recommend the workflow library.
🙏 1
e
I wrote this as a compose only solution initially, but abstracted the navigation logic and then added a compose integration. It's mostly done, and I'm still tweaking things while integrating with real world use cases. (The readme is a bit out of date) https://github.com/eygraber/portal
c
@Casey Brooks In my experience, it is simple to create your own simple router but the Androidx navigation library offers a lot more (nested graphs, deep links support, and more).
c
Oh there’s absolutely going to be many more features in it, but it’s not without its drawbacks. Especially when it comes to using it for Compose, you lose some of its helpful features like viewing a visual diagram of the graph, plus the fact that it doesn’t work on Compose for Desktop or Web, so one naturally starts thinking of alternatives. And if you do start thinking of alternatives, you’re much better off looking for (or creating) an option that uses state-based navigation completely separated from the View, rather than trying to recreate the Androidx Navigation lib with all its problems
đź’Ż 3