Navigating with Jetpack Compose

Jayant Kumar🇮🇳
10 min readApr 28

--

Photo by Jamie Street on Unsplash

In this article we are going to learn how we can navigate from one screen (composable function) to another screen or any activity in jetpack compose.

For that we have to include one library in your build.gradle file.

dependencies {
def nav_version = "2.5.3"

implementation("androidx.navigation:navigation-compose:$nav_version")
}

Before moving to the actual code , let’s learn about some topics which will be responsible for the navigation.

NavHostController

This person plays a very important role in navigation , when you navigate from one screen to another screen this will keep track the back stack of composable function and also keep the state of each screen.

Through rememberNavController() composable function we make navHostController .

val navController = rememberNavController()

Key Features of NavHostController

  • it provide methods for navigating between different destinations.
  • it manages the back stack of composable functions.
  • it provide methods for passing data between screens.

NavHost

NavHost is a composable function which is used to create navigation graph that defines the navigation structure of your app. Basically it will take screens (composable function) inside it and tells you where you have to navigate.

it takes three main parameters inside it.

  • navHostController :- it will manage the backstack and keeps the state of composable function.
  • startDestination :- it takes string type of data , which defines the starter or first screen in the navigation graph.
  • NavGraphBuilder.() :- it is a higher order extension function of NavGraphBuilder class and inside it we pass all the composable function that we want to include in the navigation graph.
val navController = rememberNavController()

NavHost(navController = navController, startDestination = "First"){
composable("First"){
FirstScreen()
}
composable("Second"){
SecondScreen()
}
}

As you can see in the above code , first we defined a navHostController after that in the NavHost function we passed navController , startDestination and all the composable functions.

composable is an extension function of NavGraphBuilder which takes key and a composable function.

Here First and Second is a key , which identifies a particular screen , also through that keys we can navigate from one screen to another.

FirstScreen and SecondScreen is a composable function.

@Composable
fun ComposeNavigation() {

val navController = rememberNavController()

NavHost(navController = navController, startDestination = "First"){
composable("First"){
FirstScreen()
}
composable("Second"){
SecondScreen()
}
}

}


// Pass above function in the MainActivity's setContent function :)
setContent{
ComposeNavigation()
}

Navigate from one screen to another screen

Now let’s see how we can navigate from one screen to another screen. As i told you above navHostController will be responsible for navigation.

@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(onClick = {
navHostController.navigate("second")
}) {
Text(text = "Screen one")
}
}
}
@Composable
fun SecondScreen() {

Text(text = "Second screen")

}

In the above code , I want to navigate from first screen to second screen. So for that i passed navHostController as a parameter and on button click just called navigate function which takes key as parameter i,e, second and navigate to second screen.

Navigate from Composable function to Activity…?

If you want to navigate to any activity from composable function, just simply fired the intent .

@Composable
fun SecondScreen() {
val context = LocalContext.current

Button(onClick = {
val intent = Intent(context, AnotherActivity::class.java)
context.startActivity(intent)
}) {
Text(text = "Move to activity")
}

}

Manage Back stack of composable functions

  • navigateUp() :- Suppose you navigate from first screen to second and you want to get back to previous screen without adding the composable function to the back stack. In that case you can use navigateUp() function.
@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.navigate("second")
}) {
Text(text = "Screen one")
}
}
}

@Composable
fun SecondScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
IconButton(onClick = {
navHostController.navigateUp()
}) {
Icon(Icons.Default.ArrowBack, contentDescription = "", tint = Color.Black)
}
}
}

As you can see in the above code in first screen , on button clicks we are calling the navigate("second") function to navigate to the second screen as I already mapped the second key with Second Screen in the navigation graph.

Here if you don’t use navigateUp() function instead of , you use navigate("first") function to get back to the previous screen then it will add the first and second screen multiple times to the backstack .

  • launchSingleTop :- If the instance of the composable function already exits at the top of the backstack , and if we set the launchSingleTop to true then it will not create the another instance of that composable function.

launchSingleTop is an alternate of android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP of activity.

@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.navigate("first") {
launchSingleTop = true
}
}) {
Text(text = "Screen one")
}
}
}

As you can see in the above code on button click we are navigating to first screen ( adding first screen to the backstack) and at the last parameter we have an Extension higher order function of NavOptionsBuilder where we are setting the launchSingleTop to true.

Now there will be only one instance of first screen will add on the backstack.

Note :- If you set the launchSingleTop to false then multiple instances of the first screen will be added on the backstack ! You can try it.

  • popUpTo() :- This function is used to define the navigation behaviour of your app. Suppose If you want to remove any composable function from the backstack , you can do with this function. Let’s understand through an example.

Suppose you have three screen A -> B -> C , and you are navigating from A to B , B to C and C to A , and you want whenever i navigate from C to A , all the composable function in the backstack should be removed. If i press back from A screen it should not moved to C again. This type of behaviour can be achieved with popUpTo() function.

@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.navigate("second")
}) {

Text(text = "Screen one")
}
}
}

@Composable
fun SecondScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.navigate("third")
}) {
Text(text = "Screen Two")
}
}
}

@Composable
fun ThirdScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
IconButton(onClick = {
navHostController.navigate("first") {
launchSingleTop = true
popUpTo("first")
}
}) {
Icon(Icons.Default.ArrowBack, contentDescription = "", tint = Color.Black)
}
}
}

As you can see in the above code , on button click of ThirdScreen composable function , we are navigating to FirstScreen and popping up or removing all the composable function from the back stack upto FirstScreen .

Note :- Here launchSingleTop = true means there will be only one instance of FirstScreen will be on top of the back stack.

  • inclusive :- It states that the popUpTo destination should be popped from the back stack.

Did not understand anything ? not a issue.

inclusive always used with popUpto() function. Basically popUpTo() function takes two parameter , String/Int and higher order extension function of PopUpToBuilder class.

Let’s understand inclusive through an example !

Suppose if i remove launchSingleTop = true from the above code then there will be two instance of FirstScreen composable function in the back stack.

So if i use inclusive = true with popUpTo function then only one instance will be there. This is how we have to use.

popUpTo("first"){
inclusive = true
}

Passing data between screen

Now the main thing is that how we can pass data from one screen to another screen. So there are multiple ways to pass data between screens. Let’s understand each one by one.

1). Through Routes

The first way to pass data through routes , it is same as like api endpoints. Let’s see an example , first we have to define the routes through sealed class .

sealed class Screen(val route: String) {

object First : Screen("first_screen")

object Second : Screen("{name}/second_screen") {
fun sendName(name: String) = "$name/second_screen"
}

}

As you can see in the above code we have defined the route (unique key) for first screen and for the second screen i want to pass the string type data from first screen to the second screen , that’s why second screen is accepting the string type data. {name} is a way to pass data.

@Composable
fun ComposeNavigation() {

val navController = rememberNavController()

NavHost(navController = navController, startDestination = Screen.First.route) {
composable(Screen.First.route) {
FirstScreen(navController)
}
composable(Screen.Second.route) {
val name = it.arguments?.getString("name") ?: "no name"
SecondScreen(navController, name)
}
}

}

Now as you can see in the above code we have passed the sub classes (First and Second) of sealed class in the NavHost composable function that will uniquely identify the screens.

And in the second screen we are getting the data through arguments which is coming from NavBackStackEntry class.

@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.navigate(Screen.Second.sendName("Jayant"))
}) {

Text(text = "Screen one")
}
}
}

@Composable
fun SecondScreen(
navHostController: NavHostController,
name: String
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Text(text = "My name is $name")
}
}

Now In the first screen on button click we are passing the name data to the second screen .

As the above way is fine to pass data between screens but it’s little difficult to pass object of data , so for that we have to use another way.

2). Through NavHostController

As I told you above NavHostController give us methods for passing data between screen. Let’s pass object of data between screens.

plugins {
id 'kotlin-parcelize'
}

Add kotlin-parcelize plugin in your build.gradle file , because first we have to parcelize the data class

@Parcelize
data class User(
val name: String,
val age: String
):Parcelable

Make the data class and parcelize it.

sealed class Screen(val route: String) {
object First : Screen("first_screen")
object Second : Screen("second_screen")
}

Define the routes through sealed class and pass it in the NavHost composable function.

@Composable
fun FirstScreen(
navHostController: NavHostController
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
navHostController.currentBackStackEntry?.savedStateHandle?.set(
"data",
User("Jayant", "23")
)
navHostController.navigate(Screen.Second.route)
}) {

Text(text = "Screen one")
}
}
}

In the above code we set the data and passing data through navHostController .

@Composable
fun SecondScreen(
navHostController: NavHostController,
) {
val data =
navHostController.previousBackStackEntry?.savedStateHandle?.get<User>("data") ?: User(
"",
""
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Text(text = "My name is ${data.name} and age is ${data.age}")
}
}

Now In the second screen we are extracting the data through navHostController and make sure to use elvis operator to get rid from null pointer exception.

3). Through ViewModel

You can also pass data through viewmodel , let’s see an example.

class NavigationViewModel : ViewModel() {

private val _data: MutableState<User?> = mutableStateOf(null)
var data: State<User?> = _data
private set

fun setData(user: User) {
_data.value = user
}

}

As you can see in the above code first we created a viewmodel . data variable will expose the data in the second composable function and setData() function will get data from the first screen .

@Composable
fun ComposeNavigation() {

val navController = rememberNavController()
val viewModel: NavigationViewModel = viewModel()

NavHost(navController = navController, startDestination = Screen.First.route) {
composable(Screen.First.route) {
FirstScreen(navController, viewModel)
}
composable(Screen.Second.route) {
SecondScreen(navController, viewModel)
}
}

}

Now In the navigation graph we created an object of NavigationViewModel through viewModel() composable function.

@Composable
fun FirstScreen(
navHostController: NavHostController,
viewModel: NavigationViewModel
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Button(onClick = {
viewModel.setData(User("Jayant", "23"))
navHostController.navigate(Screen.Second.route)
}) {

Text(text = "Screen one")
}
}
}

@Composable
fun SecondScreen(
navHostController: NavHostController,
viewModel: NavigationViewModel
) {
val data = viewModel.data.value
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Text(text = "My name is ${data?.name} and age is ${data?.age}")
}
}

Now In the above code we are setting the data in the first screen and observing the data in the second screen .

And Make sure you pass the same object of viewmodel to both the screens.

From my point of views , ViewModel is the perfect way to pass data between composable functions.

That’s all for today my friends , Hope you enjoyed and learnt something new from this article.

If you feel this article informative , feel free to clap and follow us on medium to read such type of contents.

--

--

Jayant Kumar🇮🇳

My name is jayant, i am a youtuber and software Engineer from India 🇮🇳