1

When programming for Jetpack Compose, I've been wondering why use Navigation at all. Wouldn't it be simpler to just hoist a state, say currentRoute and then use a when {} block to render the appropriate screens?

What are the upsides of using navigation with compose?

With Navigation

@Composable
fun SetupNavigation() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = Routes.Home.route,
    ) {
        composable(
            route = Routes.Home.route,
        ) {
            HomeScreen(
                rollDice = {
                    val face = Random.nextInt(1..6)
                    navController.navigate(Routes.Show.route.replace("{${Routes.Show.arguments[0].name}}", face.toString()))
                }
            )
        }
        composable(
            route = Routes.Show.route,
            arguments = Routes.Show.arguments
        ) { backStackEntry ->
            val face = backStackEntry.arguments?.getInt(Routes.Show.arguments[0].name)
            ShowScreen(
                face = face ?: -1,
                navigateHome = { navController.navigate(Routes.Home.route) },
            )
        }
    }
}

sealed class Routes(val route: String) {
    object Home: Routes("home")
    object Show: Routes("show/{face}") {
        val arguments = listOf(navArgument("face") { type = NavType.IntType })
    }
}

Without navigation (simply hoisting a currentRoute state)

@Composable
fun SetupNavigation() {
    var currentRoute by remember { mutableStateOf(Route.Home as Route) }

    when (currentRoute) {
        Route.Home -> {
            HomeScreen(
                rollDice = {
                    val face = Random.nextInt(1..6)
                    currentRoute = Route.Show(face)
                }
            )
        }
        is Route.Show -> {
            val face = (currentRoute as Route.Show).face
            ShowScreen(
                face = face,
                navigateHome = { currentRoute = Route.Home },
            )
        }
    }
}

sealed class Route() {
    object Home: Route()
    class Show(val face: Int): Route()
}

Why?

I can only see upsides in this approach without navigation:

Navigation Hoisting currentRoute
Routes are strings (prone to, for instance, typo and forgotten routes) Routes are full objects (plus sealed class won't let you miss a branch in when
Arguments are necessarily simple types Arguments can be anything the route object can hold
Argument names are strings (runtime check only) Arguments are formal properties in route object (compiler checks)
For actual navigation arguments must be marshalled (see .navigate(Routes.Show.route.replace("{${Routes.Show.arguments[0].name}}", face.toString()))) Compare currentRoute = Route.Show(face)

I don't think it would be difficult to have a stack of routes (instead of just a single current route). The same regarding handle the back button.

The rest of the code

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SetupNavigation()
        }
    }
}

@Composable
fun HomeScreen(rollDice: () -> Unit) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center,
    ) {
        OutlinedButton(onClick = rollDice) {Text("Roll dice") }
    }
}

@Composable
fun ShowScreen(face: Int, navigateHome: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        Text(
            text = "$face",
            fontSize = MaterialTheme.typography.titleLarge.fontSize,
        )
        Spacer(modifier = Modifier.size(24.dp))
        OutlinedButton(onClick = navigateHome) { Text("Back") }
    }
}

Note: I'm new to Android Programming and Jetpack Compose. Having learned just the basics about the older layout xmls (and navigation graph, data binding and so on), I've quickly switched to Compose (I think I don't need to enumerate why).

rslemos
  • 2,454
  • 22
  • 32
  • I have seen this question before https://stackoverflow.com/a/70580531/6805392 – F.Mysir Apr 28 '23 at 17:19
  • Well... then I was not the first to find it awful to: 1. designate routes by string (that kind of stipulates an implicit ABI, undetectable until runtime) and 2. accept only basicly typed arguments (sometimes, but not always, leading to an implicit ABI within marshalling/unmarshalling). I hope they (navigation-compose guys) are working on these issues. – rslemos May 04 '23 at 13:38

1 Answers1

1

if i understand your question correclty

1 - by doing this, it means that using when should be checked again for every change in navigation. But the way it is now, you only navigate to one Route. Now, if it does not exist, the error related to it will be told to you in Runtime

2 - If you don't use the main Navigation, you will miss features like SaveStateHandle, which are very useful.

3 - if you want to pass a specific result to previous screen data from second screen (example => in a authentication flow, login, register, otp, forgot password ). You can use backStackEntry and etc but with custom navigation, this is not possible (it doesn't make sense)

Vahid Garousi
  • 546
  • 4
  • 17
  • I'm now sure if I understand it correctly, but your 1st point I actually see (and listed in the original post) as an advantage: yes I want to be forced to change the `when` block whenever I create a new route. For standard Navigation this also applies. It just lack the help of the compiler. A wrong implementation leads to app crashing. – rslemos Apr 28 '23 at 14:02
  • For your 2nd point, I didn't know about `SaveStateHandle` (actually I don't now much about standard Navigation). I still need to understand if I need it. For the time being I've tried my application (with the custom navigation) and I see my solution doesn't survive reconfiguration. Maybe if I store the `currentRoute` in a viewmodel I could overcome it. I'll try. And I'll also learn about standard navigation more in-depth. – rslemos Apr 28 '23 at 14:04
  • About the 3rd point, all the examples I've seen there was never that thing as "pass result to previous screen" (in that sense of "returning a value"). In all examples it was always a "navigate forward" (but to a route that just happen to be the previous one), with a required argument passed in (plus pop some state out of the navigation stack). – rslemos Apr 28 '23 at 14:07