Implementing Navigation in Android Applications using Compose: A Comprehensive Guide

Implementing Navigation in Android Applications using Compose: A Comprehensive Guide

Photo Credit by Android Developers Blog

A brief overview of Jetpack Navigation for Android app development.

The days of using intent in navigating from one screen to another are long gone. With Jetpack Navigation Library you can do some cool stuff such as passing and sharing data between screens, navigating from one screen to another, and much more.

Jetpack Navigation is a library that offers navigation features in compose. This library makes it possible to handle navigation in apps from one screen to another.

Using the Jetpack Navigation Library as a basic framework enables developers to build easier user interfaces. This can be added to your mobile application to improve navigation ranging from button clicks to more complex navigation patterns.

Everything you need to know about Jetpack Navigation and how it works will be covered in this guide.

Prerequisite

  • You have Android Studio installed on your PC

  • Ensure you are familiar with Android Studio.

  • Ensure you know Kotlin programming language.

  • You know how to create Composable files and functions.

Setting up the Project

Adding the necessary dependencies and configurations for navigation.

Get access to source code on GitHub

  • To get started you need to create a new project or you can work on an already existing project.

  • Set your project name>packagename>savelocation>minimum SDK>build configuration.

  • The following Jetpack Compose navigation dependency should be added in your build.gradle.kts(Module:app)

val nav_version = "2.6.0"

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

implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-beta01")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-beta01")

Copy and paste the project GitHub repository here

The benefits of using Navigation Components for screen Navigation.

The benefits of using a navigation component for screen navigation cannot be overemphasized.

With Jetpack Navigation everything has been made more accessible by simply defining the destinations of screens that you want to navigate to in your mobile application. These screens are defined inside a composable. The benefits of using the navigation Component are listed:

  1. Handles the back stack ensuring users navigate back and forth through the app seamlessly.

  2. It allows the passing of data between screens during navigation.

  3. It also incorporates navigation UI patterns such as navigation drawer, and bottom navigation.

  4. Navigation components are consistent as they promote a unified user experience.

  5. Handles deep linking: this enables users to open specific screens within your app by clicking a web page or URL from external sources.

  6. The navigation component simplifies handling fragment transactions. They are used to add, replace, or remove fragments within an activity. However, navigation can handle these transactions using a declarative approach such as implementing the navigation graph.

Navigation Components in Compose

  • NavHost

Take the NavHost as a container that hosts the content for each screen in your app. You can also pass data or arguments within the NavHost.

  • NavController

The primary Job of the NavController is handling navigation actions such as button clicks to navigate to the next screen. it manages navigation within the NavHost, as well as handling back-stack.

  • NavGraph

Defines the different destination(screens) in your app i.e. the route.

  • NavBackStackEntry

The nav back stack entry is created when navigating to the destination screen for example when the user presses the back button, the most recent entry is popped from the stack.

  • Composable Route

In Compose, screens are represented as composables. The routes are incorporated in each composable which allows you to navigate to them.

Implementing Simple Screen-To-Screen Navigation.

This outline will teach you how to implement screen-to-screen navigation using code snippets. You will create a simple UI application with just 3 screens implementing the navHost controller.

Code examples and explanations of navigating using NavController

Create 3 screens composable files under the UI package folder

Screen1

@Composable
fun Screen1(navController: NavController){
   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {

       Button(onClick = {navController.navigate("screen_2")},
           colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.onPrimaryContainer),
           shape = RoundedCornerShape(12.dp)
       ) {
           Text(text = "Screen 1")

       }

   }
}

In the above code, the navController is passed as a parameter this will help you navigate to the next screen. When the button is clicked it uses the navController to navigate to the screen_2 that is specified in the route or destination. The route takes a string as a parameter.

Screen2


@Composable
fun Screen2(navController: NavController){
   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {

       Button(onClick = { navController.navigate("screen_3") },
           colors = ButtonDefaults.buttonColors(Color.Red),
           shape = RoundedCornerShape(12.dp)
       ) {
           Text(text = "Screen 2")

       }
   }
}

In the above code, the navController is passed as a parameter this will help you navigate to the next screen. When the button is clicked it uses the navController to navigate to Screen_3 that is specified in the route or destination. The route takes a string as a parameter.

Screen 3



@Composable
fun Screen3(navController: NavController){
   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {

       Button(onClick = { navController.navigate("screen_1") },
           colors = ButtonDefaults.buttonColors(Color.Green),
           shape = RoundedCornerShape(12.dp)
       ) {
           Text(text = "Screen 3")

       }

   }
}

In the above code, the navController is passed as a parameter this will help you navigate back to Screen_1. When the button is clicked it uses the navController to navigate back to Screen_1 that is specified in the route or destination. The route takes a string as a parameter.

AppNavigation


@Composable
fun AppNavigation(){
   val navController = rememberNavController()
   NavHost(navController = navController, startDestination = "screen_1"){
       composable("screen_1"){
           Screen1(navController)
       }
       composable("screen_2"){
           Screen2(navController)
       }
       composable("screen_3"){
           Screen3(navController)
       }
   }
}

The AppNavigation is the composable function that handles the navigation of the app. It houses the content for each screen and states the destination.

The navController is an instance of the NavController which enables navigation between the screens.

The rememberNavController allows the navcontroller to be remembered and recompose across the composable hierarchy. The composable specifies the screen destination and content of the screen.

Main Activity


class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceSt ate: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {

           AppNavigation()


       }
   }
}

In the main activity, you call the AppNavigation function and build your application.

Passing Data Between Screens

Demonstrative code snippets for data sharing.

Screen1

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Screen1(navController: NavController) {
   var name by remember { mutableStateOf("") }
   var age by remember { mutableStateOf("") }
   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Text(
           text = "Screen 1 ", fontSize = 30.sp
       )
       Spacer(modifier = Modifier.height(20.dp))

       OutlinedTextField(value = name, onValueChange = {
           name = it
       }, placeholder = { Text(text = "Enter your name") }

       )
       Spacer(modifier = Modifier.height(20.dp))

       OutlinedTextField(value = age, onValueChange = {
           age = it
       }, placeholder = { Text(text = "Enter your age") }

       )


       Spacer(modifier = Modifier.height(30.dp))



       Button(
           onClick = { navController.navigate("screen_2/ $name/$age") },
           colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.onPrimaryContainer),
           shape = RoundedCornerShape(12.dp)
       ) {
           Text(text = "Screen 1")

      }

   }
}

In the above code, name and age were declared as mutable states and initialized it with empty strings. When the user inputs their details in the text fields and clicks the button, the data entered is passed to the second screen with the specified destination.

Screen 2


@Composable
fun Screen2(
   name: String?,
   age: Int?
){
   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Text(text = "Screen 2 Details",
           fontSize = 30.sp)
       Spacer(modifier = Modifier.height(30.dp))

       Text(text = "My name is: $name",
           fontSize = 20.sp)
       Spacer(modifier = Modifier.height(30.dp))

       Text(text = "My age is: $age",
           fontSize = 20.sp)

   }
}

In the above code, the screen2 composable function is designed to display the data passed from screen1 when it is invoked. The name and age parameters are marked as nullable. When both parameters are not null they will display the data passed in.

AppNavigation

@Composable
fun AppNavigation() {
  val navController = rememberNavController()
  NavHost(navController = navController, startDestination = "screen_1") {
      composable("screen_1") {
          Screen1(navController)
      }
      composable(
          "screen_2/{name}/{age}",
          arguments = listOf(
              navArgument(name = "name") {
                  type = NavType.StringType
              },
              navArgument(name = "age") {
                  type = NavType.IntType
              }
          )
      ) { backstackEntry ->
          Screen2(
              name = backstackEntry.arguments?.getString("name"),
              age = backstackEntry.arguments?.getInt("age")
          )
      }

  }
}

In the above code, the startdestination represents the initial destination where your application will start upon launching. The composable defines the route or destination of the screen you want to navigate. We passed the navController to the screen_1 which helps in the navigation.

In Screen2 the destination is marked with keys for both values. It tells the navController when navigating to pass the value for name and age to the screen2.

The arguments represent the list of arguments associated with the destination, the type of data you are passing is declared here, with the key.

The backstackEntry gets the value passed for the name and age that will be displayed on screen_2.

Main Activity

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContent {

          AppNavigation()


      }
  }
}

Call the navigation function and build your app

Note: Two types of data can be passed into screens in navigation. We have the required data and optional data.

In required data, you must pass in the data as argument else your navigation will fail. This will make your app crash. The above example shows us the required type of data in navigation. When you leave one of the fields empty and click on the button your application will stop.

Using optional data involves setting default values or making your arguments nullable, this data tagged as nullable enables the screen navigation function properly when data is not available. Optional data is used when you want to give the user the option to leave some fields empty I will show us how to implement the optional data for screen navigation below.

Using the same code apply some changes to display the implementation of the optional data in the screen navigation.

In the onClick set the name and age


composable(
  "screen_2?name={name}&age={age}",
  arguments = listOf(
      navArgument(name = "name") {
          type = NavType.StringType
          //defaultValue ="user" //set a default value
          nullable = true
      },
      navArgument(name = "age") {
          type = NavType.IntType
          defaultValue = 0
      }
  )

In the list of argument, you set a default value for the parameters. When you launch the application if the user does not enter any details the default values set will be displayed on screen 2.

Providing ViewModel supports using navigation graphs to share UI-related data between the graph's destinations.

Passing data between screens during navigation is made possible using the view model. The ViewModel is responsible for holding, storing, and managing shared data and also ensures that data persists when the user navigates to other screens within the application.

It is a crucial concept that every developer must know when building larger Apps with complex navigation flows. Let's dive right into it.

Create a data class to model the data you want to pass to the screen.

The data class represents and manages the data in a well-organized manner.

Data class

data class Details(
  var name: String,
  var age: String,
  var schoolName: String
)

ViewModel class

class MyViewModel:ViewModel() {
  private  var state = MutableStateFlow(
      Details(
          "",
          "",
          ""
      ))


  val stateFlow: StateFlow<Details> = state
  fun save(details: Details) { //this function will update the data passed in screen 2 to screen 1
      _state.value = details


  }

}

In the above code, we created the ViewModel Kotlin class and a function to save the user details to be passed on to Screen_1.

We declared a private variable _state and initialized it with an object of the data class. The MutableStateFlow emits values to the observers and the stateFlow variable allows the app to observe and receive updates when the state of the Ui is changed.

The _state.value gets the value entered by the user and updates it.

Screen1


@Composable
fun Screen1(navController: NavController, viewModel: MyViewModel) {
   val state by viewModel.stateFlow.collectAsStateWithLifecycle()

   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Text(
           text = "Screen 1 ", fontSize = 30.sp
       )

       Spacer(modifier = Modifier.height(20.dp))

       Text(
           text =
           "${state.name}"
       )

       Spacer(modifier = Modifier.height(20.dp))

       Text(
           text = "${state.age}"
       )

       Spacer(modifier = Modifier.height(20.dp))
       Text(
           text =
           "${state.schoolName}"
       )

       Spacer(modifier = Modifier.height(40.dp))

       Button(
           onClick = {
               navController.navigate("screen_2"

               )
                     },
           colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.onPrimaryContainer),
           shape = RoundedCornerShape(12.dp)
       ) {
           Text(text = "Screen 1")

       }

   }
}

The viewmodel is an instance of the MyViewModel class. The navcontroller helps in the navigation process to screen_2. We declared a variable state and assigned it to the instance value of our viewmodel. The stateFlow.collectAsStateWithLifecycle() associates the state variable with the Composable's lifecycle.

Screen 2


@SuppressLint("SuspiciousIndentation")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Screen2(navController: NavController, viewModel: MyViewModel) {

  var name by remember { mutableStateOf("") }
  var age by remember { mutableStateOf("") }
  var schoolName by remember { mutableStateOf("") }
  Column(
      modifier = Modifier.fillMaxSize(),
      verticalArrangement = Arrangement.Center,
      horizontalAlignment = Alignment.CenterHorizontally
  ) {
      Text(
          text = "Screen 2 ", fontSize = 30.sp
      )

      Spacer(modifier = Modifier.height(20.dp))
      OutlinedTextField(value = name, onValueChange = {
          name = it
      }, placeholder = { Text(text = "Enter your name") },
          maxLines = 1

      )

      Spacer(modifier = Modifier.height(20.dp))
      OutlinedTextField(value = age, onValueChange = {
          age = it
      }, placeholder = { Text(text = "Enter your age") },
          maxLines = 1

      )

      Spacer(modifier = Modifier.height(20.dp))
      OutlinedTextField(value = schoolName, onValueChange = {
          schoolName = it
      }, placeholder = { Text(text = "Enter your School name") },
          maxLines = 1

      )


      Spacer(modifier = Modifier.height(40.dp))



      Button(
          onClick = {
        val details = Details(
                name = name,
                age = age,
                schoolName = schoolName
            )
              viewModel.save(details)
navController.popBackStack()

                    },
          colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.onPrimaryContainer),
          shape = RoundedCornerShape(12.dp)
      ) {
          Text(text = "Save")
      }
  }

}

In the above code, there is an instance of the ViewModel class and NavController.

The variables are marked with mutable states to trigger recomposition when the UI data changes.

In the onclick, an object of the details class was created. The object is then passed to the viewmodel.save function to store the details entered by the user in the view model. Once the details are saved the navController.popBackStack() is called to return to the previous screen where you will see the details entered by the user displayed.

AppNavigation

@Composable
fun AppNavigation(){
  var navController = rememberNavController()
  var vm = viewModel<MyViewModel>()

  NavHost(navController = navController, startDestination = "screen_1"){
      composable("screen_1"){
          Screen1(navController,vm)
      }
      composable("screen_2"){
          Screen2(navController, vm)
      }
  }
}

In the above code, an instance of the Navcontroller and view model class was created, and these instances are passed to the composable containing the screen destination to display the contents on the screen.

Main Activity

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

              }
          }
      }
  }

In the main Activity, the AppNavigation function is called, and after that build your app.

An example of a Native Android application that uses Jetpack Navigation is the Popular Thread App

Wrap Up

In this article, you have learned what Jetpack Navigation is all about and its implementation in various cases.

We discussed

  • A brief overview of jetpack navigation.

  • Navigation Components.

  • How to implement navigation from screen to screen by demonstrating using code.

  • How to pass data using screen navigation.

  • Providing ViewModel support using navigation graphs to share UI-related data between the graph's destinations.