Blog Infos
Author
Published
Topics
,
Published

In most modern apps, some form of navigation has become a foundational requirement. Thus, Google introduced Jetpack’s Navigation Component which is responsible for handling navigation from one destination to another in Android apps and now provides support for applications written using Jetpack Compose. In this tutorial, we’ll take a beginner’s look at how to implement Navigation in Jetpack Compose. We will also take a look at how to pass parameters between screens, as well as, implement the functionality for the user to be able to navigate to the previous screen when clicking on the back button in the toolbar.

We will build an app called Country Fact which is based on a previous navigation article I wrote. Our app will display a list of countries to a user and will display a details screen when a user clicks on any of the countries. Our detail screen will display some statistics relating to the selected country. In this tutorial we will primarily focus on Navigation Compose thus some of the other code has been pre-written for you. Please explore the app to familiarize yourself with the starter code.

Import the starter project into Android Studio. Once done, Build and run the app. You’ll see a list of countries being displayed. You will be able to scroll through the countries list and when you click on a country a toast message will appear showing the selected country’s unique ID. Our first objective is to add the required Gradle dependencies after which we will add the functionality for the user to navigate to the detail screen when a country is clicked.

To use the Navigation component we need to add some dependencies. You can get the latest dependency here. At the time of writing this article, the below was the latest, so go ahead and add it to your build.gradle. Once added sync your project and you should now be able to use Navigation in Compose:

implementation("androidx.navigation:navigation-compose:2.4.0-rc01")

Our next step is to add the functionality to navigate to the detail screen once the user selects a country in our list.

Let’s first create a new package called “navigation” within our main package.

Within our new package create a Kotlin Sealed class called “CountryScreens”. We will use this class to specify all the screens that we will be navigating to within our application. Add an immutable String parameter called “route” to the CountriesScreen class. Our “route” is used to determine which screen our application should navigate to. For example, in our case, we will pass a value through the “route” indicating that the destination should be either the home screen or the detail screen. You will understand a bit better once we implement the code.

sealed class CountryScreens(val route: String) {
}

Next, within the CountryScreens class add all the screens that we will be navigating to as shown below:

sealed class CountryScreens(val route: String) {
    object HomeScreen : CountryScreens("home_screen")
    object DetailScreen : CountryScreens("detail_screen")
}

Great work so far! For now, we have done enough in the CountryScreens class. We will, however, come back to this class a bit later when we learn how to pass arguments between screens.

Creating the NavController

Within the same package (“navigation”) create a Kotlin File called “Navigation”.

Create a Composable function called “Navigation” within the file we create:

@Composable
fun Navigation(){
    
}

Within our Navigation composable function we will first create our NavController as can be seen below:

val navController = rememberNavController()

The NavController is the backbone of being able to navigate in Compose. It can be used to handle navigation between screens, keeps track of back stack entries, enables back stack manipulation, as well as be used to move the stack forward.

We make use of “rememberNavController()” to get an instance of the NavController. “rememberNavController()” not only creates the NavController but it also makes use of rememberSavable to enable our NavController to survive configuration changes.

Create the NavHost

Each NavController in your application must be connected to a single NavHostcomposable. The NavHost connects our NavController to a navigation graph which can be used to define all possible composable destinations a user can navigate to within our app.

Let’s add our NavHost under our NavController:

NavHost(navController = , graph = ){}

Note: If needed import the NavHost

Now what you will notice is that our NavHost requires some arguments. First, it requires a navController, and secondly, it requires a graph. Lucky for us we already created the navController so we can just pass that as our first argument. For our second argument instead of passing a graph, we will pass our start destination. Replace “graph” with “startDestination”; then for our start destination we will make use of our CountryScreens class which contains all our possible destinations in the application. To add our home screen as our start destination, see below:

NavHost(navController = navController, startDestination = CountryScreens.HomeScreen.route) {}

The above trailing lambda represents the NavGraphBuilder which we will use to specify all the destinations that our NavHost can navigate to. We will use the Navigation Compose artifact’s NavGraphBuilder.composable extension function to define all the navigation destinations. Within the NavHost’s trailing lambda add all the possible destinations to our graph as can be seen below:

composable(route = CountryScreens.HomeScreen.route) {
HomeScreen()
}
composable(route = CountryScreens.DetailScreen.route) {
DetailScreen()
}
view raw Navigation().kt hosted with ❤ by GitHub

You will also notice that we are calling the correct composable function within the lambda for each destination. For example when the chosen destination is the home screen the “HomeScreen()” composable function will be called resulting in the home screen being shown. We also need to pass the navController to our composable destinations. This will enable us to navigate to other destinations from within our composable destination. For example, we will use the navController being passed to the HomeScreen() composable to navigate to the DetailsScreen() composable. Pass the navController to our composable destinations:

composable(route = CountryScreens.HomeScreen.route) {
HomeScreen(navController = navController)
}
composable(route = CountryScreens.DetailScreen.route) {
DetailScreen(navController = navController)
}
view raw Navigation().kt hosted with ❤ by GitHub

The complete function should look as follows:

 

 

When passing the navController to our destination composable you will notice some errors popping up that we need to fix. Navigate to the HomeScreen() class (screenshome HomeScreen) and pass the navController as an argument:

@Composable
fun HomeScreen(navController: NavController) {

You’ll also need to pass the NavController to the MainContent() composable:

@Composable
fun MainContent(
    navController: NavController,
    countryList: List<Country>
)

The MainContent() composable is being called from the HomeScreen() composable function so add the navController as an argument:

MainContent(navController, country)

Next, we also need to pass the navController as an argument to the DetailsScreen() composable (screensdetail DetailsScreen):

@Composable
fun DetailScreen(navController: NavController) {

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Navigation superpowers at your fingertips

This talk will begin by the demonstration of a beautiful sample app built with Compose Mulitplatform and Appyx, complete with:
Watch Video

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android engineer
Bumble

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android en ...
Bumble

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android enginee ...
Bumble

Jobs

Finally, navigate to MainActivity() where you will notice that we are calling the HomeScreen() composable from the MainActivity(). Instead of calling the HomeScreen() composable, we will rather call the “Navigation()” composable. We are calling the Navigation composable function because it has all the information we need such as which screen should be shown first etc. Replace HomeScreen() with Navigation():

setContent {
CountriesApp {
Navigation()
}
}

NB: Do the same in the DefaultPreview() composable.

Let’s run the app to see if everything is still working fine. You will notice that nothing has changed and that we are still showing the list of countries and the country’s id on click.

Navigate back to the HomeScreen() class (screenshome HomeScreen). We need to navigate to the detail screen when we click on a country on our home screen. At the moment we are showing a toast with some information when a user clicks on an African country in our list. We need to swap that out so that we navigate to the detail screen when a country is selected.

In the MainContent() composable function, replace the toast:

Toast.makeText(context, countryId.toString(), Toast.LENGTH_SHORT).show()

with the below line of code:

navController.navigate(CountryScreens.DetailScreen.route)

Note: Import the Countryscreens class

Let’s run the app again. You should now be able to navigate to the details screen. Awesome! 💪

We are however not done yet 🙈. You will notice that the details being shown on the details screen is still the dummy data we hardcoded. We want the correct information to be displayed for the specific country selected. We need to pass the selected country’s “ID” as an argument from the home screen to the details screen. We will then use the “ID” to retrieve and display the correct information to be displayed on the details screen.

Here we want to pass the country’s id to the details screen as an argument so we can use the id to search for the country from our data source and display more information about the selected country.

Open the Navigation file (navigationNavigation). Passing arguments using Jetpack Compose Navigation works similar to how URL’s work on the web. For example, in our case, we will have the default route but if we want to access a specific country we also need to append the id of the country we want to fetch. Append the “countryId” to the DetailScreen composable’s route:

route = CountryScreens.DetailScreen.route+"/{countryId}"

The DetailScreen destination should now be as can be seen below:

As you can see we have our default route, but then we use a forward slash followed by parenthesis. We then specify the name of the argument we want to pass within the parenthesis.

Open the HomeScreen class(screenshome HomeScreen). In the MainContent() composable, refactor the code in the CountryRow’sonItemClick listener to also append “countryId”:

navController.navigate(CountryScreens.DetailScreen.route+"/$countryId")

Note: In our use case we are passing a mandatory argument. It is also possible to pass optional arguments for example if we were to pass an optional argument it would look something like this:

route = CountryScreens.DetailScreen.route+”?name={countryId}”

We are replacing the forward slack with “?name=”

Open the Navigation file (navigationNavigation). Now that we have specified that an ID called “countryId” will be passed as an argument we now need to pass the actual argument. Pass the “arguments” as can be seen below:

composable(
route = CountryScreens.DetailScreen.route + "/{countryId}",
arguments = listOf(navArgument(name = "countryId") {
type = NavType.IntType
})
)
view raw Navigation().kt hosted with ❤ by GitHub

Note: Import everything that needs to be imported

We are now specifying that we will be passing through a list of arguments. In our case, we will only be passing through a single argument named “countryId”. We are also able to specify what type the argument will be, as well as, other constraints such as whether the argument is nullable, if it has a default value and more.

The next step is to get access to the argument so that we can pass it to the details screen. We can make use of the “navBackStackEntry” to get access to the argument that needs to be passed. Rename the “navBackStackEntry” from “it” to “entry”:

Pass the argument to the detail screen by replacing:

DetailScreen(navController = navController)

With:

DetailScreen(navController, entry.arguments?.getInt("countryId"))

This will give us an error because we need to specify that the detail screen requires the country id as an argument. Navigate to the DetailScreen class(screenshome DetailScreen) and add the country id as a parameter.

@Composable
fun DetailScreen(navController: NavController, countryId: Int?)

Finally, in the DetailScreen composable function replace all the dummy data below:

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Enter Movie title",
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(10.dp))
Text(text = "Population", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Population here")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Largest City", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Largest City here")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Capital City", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Capital City here")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Major Language", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Major Language here")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Major Religion", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Major Religion here")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Monetary Unit", fontWeight = FontWeight.Bold)
Text(text = "Enter Movie Monetary Unit here")
}

With the actual data retrieved from our data source:

val country = countryId?.let { it1 -> CountryRepository.findCountry(it1) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "${country?.name} ",
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(10.dp))
Text(text = "Population", fontWeight = FontWeight.Bold)
Text(text = "${country?.population}")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Largest City", fontWeight = FontWeight.Bold)
Text(text = "${country?.largest_city} ")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Capital City", fontWeight = FontWeight.Bold)
Text(text = "${country?.capital_city} ")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Major Language", fontWeight = FontWeight.Bold)
Text(text = "${country?.major_language} ", textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Major Religion", fontWeight = FontWeight.Bold)
Text(text = "${country?.major_religion} ")
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Monetary Unit", fontWeight = FontWeight.Bold)
Text(text = "${country?.monetary_unit} ")
}

Note: Import everything that needs to be imported

When you run the app now you will be able to see all the details of the selected country. Yay!🎊 🎉 we’ve made some great progress. You can be proud!

When navigating to the details screen you’ll notice that the only way for us to go back to the previous screen is to use the hardback button. We’d like to give the users of our application some more options. We will give the user the option of navigating back to the previous screen using the back button in the toolbar.

We make use of the navigation controller to navigate from one screen to another in Compose. When we open our application, our first screen is pushed on the stack. Meaning every time we navigate to a new destination that destination gets pushed on the stack. In our case, in order to navigate to the previous screen, we only need to pop the top item from the stack (that being the details screen) for us to navigate back to the home screen.

Open the DetailScreen and make the following modification to the Detail screen’s top bar:

TopAppBar(backgroundColor = Color.Transparent, elevation = 0.dp) {
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.padding(start = 8.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Arrow Back",
modifier = Modifier.clickable {
navController.popBackStack()
})
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Country Detail", fontWeight = FontWeight.Bold)
}
}

Let’s loop through what is happening:

  • First, we add a row to the body of our TopAppBar and specify that the horizontalArrangement should be “Arrangement.Start” to left-align the content, and we also give it some padding.
  • We then add the back icon and add the functionality for the user to be able to click the icon.
  • As mentioned above we will pop the top item on the stack (Detail screen). We are making use of the navController that we passed through the constructor to handle navigation for us.
  • Finally, we create space between the back icon and the screen’s title.

And that it! Run the app again and you should now be able to navigate back to the home screen using the toolbar’s back button. ☝️

I look forward to your feedback and if for some reason, you got stuck I would advise you to take a look at the repo containing the completed project.

Thank you for reading this article. Click the 👏 button as much as you can, 🌟 the Github repo, and follow me on Twitter and Medium for future articles.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu