https://kotlinlang.org logo
#compose
Title
# compose
b

Bradleycorn

02/22/2022, 5:54 PM
Navigation Question: when navigating to a route that takes some parameters (i.e. a route like
path/to/products/{cateegory}/{id}
), I don’t want to just hard code strings throughout my application (like:
navController.navigate("path/to/products/shirts/1234")
. If that route ever changes, I have to find and update it throughout my application. I’m curious what sort of architecture others have used to address this? My current solution in the 🧵
I have my navigation components setup similar to the suggestions in the bottom Navigation example in the Navigation Compose docs. I have a sealed class,
Destination
to define my destinations, and each has a
route
parameter. Then, on each destination, I add a
buildRoute(...): String
method that can be called to build a route string with the proper formatting. So, a basic version of my sealed class looks like:
Copy code
sealed class Destination(val route: String, val title: String) {
    object Product(route="/path/to/Products/{category}/{id}", title="Product") {
        fun buildRoute(category: String, id: Int) {
            return route.replace("{category}", category).replace("{id}, id)
        }
    }
}
And when I want to navigate, I’d do something like:
Copy code
val productDestination = Destination.Product.buildRoute(myProduct.category, myProduct.id)
navController.navigate(productDestination)
How do you avoid littering your apps with destination route strings?
z

Zun

02/22/2022, 6:49 PM
There are no nice solutions. Any of them require you write code twice or work with constants/builders that can still result in compile time issues. You're better off using an alternative library for navigation. I heard Voyager is good
1
p

paul vickers

02/22/2022, 11:07 PM
I've just started using Compose Destinations Library, a lot less code to write, a lot less annoying
i

Ian Lake

02/23/2022, 5:26 AM
Yep, if you don't want to manually build type safe methods (which are indeed just those simple one liners which prevent leaking your routes out of the one file they are declared in), Compose Destinations will do that for you: https://github.com/raamcosta/compose-destinations
2
If you don't want to go that far, then you'll want to generally structure things into three layers: 1. An ideally fully KMP, type safe Screen that is fully independent of any Navigation framework (this is what our testing guide specifically recommends) - state in, events out 2. Your navigation graph in a file as a
fun NavGraphBuilder.yourGraph(onNavigateToFriendsList: (userId: String) -> Unit))
method that encapsulates the creation of the graph (what our nested graph guide specifically calls out for segmenting your graph across separate modules). Note that
NavGraphBuilder
extension should take a lambda for each external navigation call (i.e., a call to navigate defined in some other graph/module - we'll use this in part 3). That same file should define the subset of
fun NavController.navigateToProfile(userId: String)
extension methods that make up the public interface of that module 3. Your app module (or the one with the
NavHost
) then calls each
yourGraph()
method, calling the correct
navController.navigateToFriendsList(userId)
from module A when
onNavigateToFriendsList
defined in module B is called. Obviously if you have graphs nested within graphs, add layers of 1 and 2 as you build up your graph Thus you've now built out an entirely type safe API that fully defines the exact API surface you need that fully supports as many independent modules as you want. All without any idea of routes leaking out of that file in step 2 (which, if you've split up your modules correctly, is probably just a single file for that module) or your navigation code being endemic to each one of your screens (as they'd be defined independent of Navigation in step 1).
b

Bradleycorn

02/23/2022, 3:05 PM
Thanks @Ian Lake. The Navigation Component works well for me. I’m not looking to blow up my whole architecture to implement a new navigation system. The setup you’ve described is pretty close to what we are using in our app. The only piece we don’t have is the extension functions on
NavController
for making the actual nav calls. I think implementing those will be the final piece to my type-safe, DRY routes puzzle. To me the one liner type-safe nav functions are no different that the static methods we’d use to create an Intent for starting a specific Activity, or an Arguments bundle for a Fragment. Which is to say, we’ve been doing that for years and it’s fine. As they say, “If it ain’t broke, don’t fix it”…
m

mattinger

02/24/2022, 11:55 PM
@Ian Lake I've been preaching this to the devs in my org. I'm wondering if there's any best practices documentation which basically re-iterates your points so i can hammer it home more.
i

Ian Lake

02/24/2022, 11:58 PM
This is a big focus for our DevRel team through March here, so hopefully we'll have more comprehensive docs soon (you'll note how those first two points already have links to the relevant public docs)
m

mattinger

02/25/2022, 12:03 AM
I'm curious what your take on things like TopAppBar and scaffold are. I think it gets very tedious to maintain a single one across the activity, particularly if there's a very large navigation graph with a lot of screens. We've been thinking to put a scaffold on each screen individually, since we're basically just putting back and close buttons with a title up there.
FWIW, a Scaffold is quite expensive (since it uses
SubcomposeLayout
) - you probably just want a
Column
for anything inside an individual screen 🙂
m

mattinger

02/25/2022, 4:55 PM
Thanks for the tip @Ian Lake. I’ve wrapped it in a function so it’s easy enough to replace for us, but i still haven’t determined if we need additional features of Scaffold like snackbars.
s

Stylianos Gakis

06/21/2023, 9:47 AM
Coming back to this discussion, regarding the idea of having your module graphs expose something like this
fun NavGraphBuilder.yourGraph(onNavigateToFriendsList: (userId: String) -> Unit))
. This has been working very well for me, but I am now trying to do an extra thing, and would maybe like to hear your thoughts. I am scoping a VM to a graph that this feature contains as suggested here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1678472768490219?thread_ts=1678466992.068379&cid=CJLTWPH7S, but to do that I need the instance of
navController
normally, to get the NavBackStackEntry. How would you suggest this to be solved? Passing a lambda which does
(route: String) -> NavBackStackEntry
could be an option, but then gotta also be careful with not referencing a stale NavController in some way. I got something like this atm (createRoutePattern comes from the kiwi library, but don’t let that distract you, it could be a simple String containing the route directly)
Copy code
@Composable
inline fun <reified Dest : Destination, reified VM : ViewModel> destinationScopedViewModel(
  noinline getBackStackEntry: (route: String) -> NavBackStackEntry,
  backStackEntry: NavBackStackEntry,
): VM {
  val updatedGetBackstackEntry by rememberUpdatedState(getBackStackEntry)
  val parentEntry = remember(backStackEntry) {
    updatedGetBackstackEntry(createRoutePattern<Dest>())
  }
  return koinViewModel(viewModelStoreOwner = parentEntry)
}
Which can make the graph API look like this instead
Copy code
fun NavGraphBuilder.yourGraph(getBackStackEntry: (route: String) -> NavBackStackEntry, onNavigateToFriendsList: (userId: String) -> Unit)).
And called something like this from the :app module.
Copy code
yourGraph(getBackStackEntry = navController::getBackStackEntry, onNavigateToFriendsList = { navController.navigate... }),
So that you don’t pass in the NavController to the feature module and people are tempted to use that directly to navigate to places instead of using the lambda passed to it instead. Am I missing something clearly better than what I am doing here?
Also hmm, in order to also be able to not navigate away when there’s already a navigation going on, as suggested here, then the lambdas need to be in this shape
Copy code
fun NavGraphBuilder.yourGraph(getBackStackEntry: (route: String) -> NavBackStackEntry, onNavigateToFriendsList: (backStackEntry: NavBackStackEntry, userId: String) -> Unit)).
Note the addition of
backStackEntry: NavBackStackEntry
in the navigate event, so that we can also check on the lifecycle state before the navigation happens. So call site changes to
Copy code
yourGraph(
  getBackStackEntry = navController::getBackStackEntry,
  onNavigateToFriendsList = { backStackEntry, userId ->
    if (backStackEntry.lifecycle.currentState == Lifecycle.State.RESUMED) {
      navController.navigate... 
    }
  }
)
edit: Right, Jetsnack already does showcase this
132 Views