Navigation in Jetpack Compose

Chase
5 min readMay 1, 2024

Navigation in Jetpack Compose has always been on of those things that was just hard for me to wrap my head around for some reason. I was finally able to spend some time and figure it out. Let’s learn together.

The Text of Navigation In Jetpack Compose with a shuffle emoji and a robot emoji on either side of the text

Before we get started, please take a couple of seconds to follow me and 👏 clap for the article so that we can help more people learn about this useful content.

But first, dependencies

This is one of my least favorite things about Android development, but now I understand some of the why behind adding dependencies for everything. It helps keep the program as lean as it can be, and coming from the world of iOS development, it’s annoying to have to add a dependency for something that Google has made when you are working in a Google product. It would be nice to just get those for free since the odds are pretty high that your app won’t be the only thing on the device that needs a navigation library. Okay, rant over. For this project we will need to add the following dependency to get navigation added into our project, after you have added this line be sure to click the maven sync button at the top right side of the window.

dependencies {
// add the following first party dependency
implementation("androidx.navigation:navigation-compose:2.7.7")
}

Building the UI

In our project we will have two screens, a “main” screen and a “detail” screen. For now it is fine to place both inside the MainActivity.kt file. I went ahead and included all the imports that we will need. You may notice the NavigationStack composable in this file, don’t worry we will get to that soon, for now though, it does mean that our app won’t compile yet. The app we will build starts on a screen with a simple text box and a button. Once the button is pressed it will try to navigate us to a new route, the detail screen route, where we will see the text of “Detail Screen” above the text of whatever was typed into the text box. Pressing the back button or swiping back on the detail screen will take us back to the “Main Screen”, and thus a simple navigation app.

// MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavController
import io.jpmtech.basicnavexample.ui.theme.BasicNavExampleTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicNavExampleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavigationStack()
}
}
}
}
}

@Composable
fun MainScreen(navController: NavController) {
val text = remember {
mutableStateOf("")
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
OutlinedTextField(value = text.value, onValueChange = {
text.value = it
})

Button(onClick = {
navController.navigate(route = Screen.Detail.route + "?text=${text.value}")
}) {
Text("Next Screen")
}
}
}

@Composable
fun DetailScreen(text: String?) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text("Detail Screen")
Text("$text")
}
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BasicNavExampleTheme {
NavigationStack()
}
}

Building the Navigation

It is always nice to have a type safe option when trying to navigate to another screen when your routes are essentially just a string. For this reason, we have created a sealed class (similar to a final class in Swift) that allows us to use what amounts to an enum in Swift so that we don’t accidentally mistype anything when trying to go to a new screen.

// Screen.kt
sealed class Screen(val route: String) {
object Main: Screen("main_screen")
object Detail: Screen("detail_screen")
}

You may notice what we are calling a NavigationStack in our code is actually a composable function. That is because the way the Navigation Controller works in Jetpack Compose is similar to how a NavigationStack works in Swift, so to help bridge the mental gap in my own brain, I gave it the same name. The combination of the navController and the NavHost allow you to push and pop views on to and off of the stack, they just put the pieces together it in a slightly different way than we would in the iOS world.

Inside the NavHost, a view only becomes “active” (added onto the stack) when a route matches the route passed in for a given composable. For example, if a route matches the Screen.Detail.route, the NavHost will add the composable that is passed in to the lambda function (trailing closure) on to the stack and that screen will be displayed. We can also pass in parameters directly through our navigation as you can see in the Detail screen example below. However, I would argue that this shouldn’t be a best practice, and that if you need to access a variable in the view it should come from the view model. I wanted to include it here simply to let you know that it is possible though I wouldn’t recommend it.

// NavigationStack.kt
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument

@Composable
fun NavigationStack() {
val navController = rememberNavController()

NavHost(navController = navController, startDestination = Screen.Main.route) {
composable(route = Screen.Main.route) {
MainScreen(navController = navController)
}
composable(
route = Screen.Detail.route + "?text={text}",
arguments = listOf(
navArgument("text") {
type = NavType.StringType
nullable = true
}
)
) {
DetailScreen(text = it.arguments?.getString("text"))
}
}
}

Once we have this file in our project our app can compile. Running the app, we can see out navigation working in the screenshots below.

A side by side screenshot of the main screen with the text of “test” in the box beside the detail screen displaying the text of “test”

If you got value from this article, please consider following me, 👏 clapping for this article, or sharing it to help others more easily find it.

If you have any questions on the topic or know of another way to accomplish the same task, feel free to respond to the post or share it with a friend and get their opinion on it. If you want to learn more about native mobile development, you can check out the other articles I have written here: https://medium.com/@jpmtech. If you want to see apps that have been built with native mobile development, you can check out my apps here: https://jpmtech.io/apps. Thank you for taking the time to check out my work!

--

--