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).