Bradleycorn
02/22/2022, 5:54 PMpath/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 🧵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:
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:
val productDestination = Destination.Product.buildRoute(myProduct.category, myProduct.id)
navController.navigate(productDestination)
How do you avoid littering your apps with destination route strings?Zun
02/22/2022, 6:49 PMpaul vickers
02/22/2022, 11:07 PMIan Lake
02/23/2022, 5:26 AMfun 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).Bradleycorn
02/23/2022, 3:05 PMNavController
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”…mattinger
02/24/2022, 11:55 PMIan Lake
02/24/2022, 11:58 PMmattinger
02/25/2022, 12:03 AMIan Lake
02/25/2022, 12:15 AMSubcomposeLayout
) - you probably just want a Column
for anything inside an individual screen 🙂mattinger
02/25/2022, 4:55 PMStylianos Gakis
06/21/2023, 9:47 AMfun 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)
@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
fun NavGraphBuilder.yourGraph(getBackStackEntry: (route: String) -> NavBackStackEntry, onNavigateToFriendsList: (userId: String) -> Unit)).
And called something like this from the :app module.
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?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
yourGraph(
getBackStackEntry = navController::getBackStackEntry,
onNavigateToFriendsList = { backStackEntry, userId ->
if (backStackEntry.lifecycle.currentState == Lifecycle.State.RESUMED) {
navController.navigate...
}
}
)
edit: Right, Jetsnack already does showcase this ✅