Android : Handle Navigation for Android Compose
Navigation Compose
Nowadays, Mulitiscreen App is a basic App to develop, because in Apps need some screen to show the detail, setting, splash, authentication and any feature that you need. The Navigation Compose component allows you to easily build multi screen apps in Compose using a declarative approach, just like building user interfaces.
Navigation Component Environment & Library
libs.versions.toml
[versions]
...
navigationCompose = "2.8.5"
serialization = "1.6.3"
[libraries]
...
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
[plugins]
...
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
app/build.gradle.kts
plugins {
...
alias(libs.plugins.kotlin.serialization)
}
dependencies {
...
implementation(libs.navigation.compose)
implementation(libs.kotlinx.serialization.json)
}
build.gradle.kts
plugins {
...
alias(libs.plugins.kotlin.serialization) apply false
}
Create Screen Interface
Create Sealed Interface to store all of your screen to avoid type mistake within serializable.
Use data object
for default screen and data class
for screen with parameter.
For example:
Screen.kt
import kotlinx.serialization.Serializable
sealed interface Screen {
@Serializable
data object A: Screen
@Serializable
data class B(val title: String): Screen
}
Setup Navigation
The Navigation component has three main parts:
- NavController: Responsible for navigating between destinations—that is, the screens in your app.
- NavGraph: Maps composable destinations to navigate to.
- NavHost: Composable acting as a container for displaying the current destination of the NavGraph.
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
Navigation()
}
}
}
}
@Composable
fun Navigation(){
// important part and must declare fisrt
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.ScreenA // Set Screen to appear for the first time
){
// Declare all Screen in here
composable<Screen.A>{
ScreenA()
}
composable<Screen.B>{
ScreenB()
}
}
}
Basic behavior
These are the core facts you should consider regarding the behavior of the navigation stack:
First destination
: When the user opens the app, the NavController pushes the first destination to the top of the back stack.Pushing to the stack
: Each callNavController.navigate()
pushes the given destination to the top of the stack.Popping top destination
: Tapping Up or Back calls theNavController.navigateUp()
andNavController.popBackStack()
methods, respectively. They pop the top destination off the stack.
Simple Navigate
In case for simple navigation that just move to next screen without pass any argument and push to the top of the stack.
For Example like this 2 screen. I want to make navigation like Screen A -> Screen B
:
@Composable
fun ScreenA(
onNavBClick: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
onNavBClick()
}
) {
Text("Move to Screen B")
}
}
}
@Composable
fun ScreenB(
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Screen B")
}
}
use this method to navigate
navController.navigate(route)
MainActivity.kt
NavHost(
...
){
composable<Screen.A>{
ScreenA(
onNavBClick = {
navController.navigate(Screen.B)
}
)
}
composable<Screen.B>{
ScreenB()
}
}
Navigate and Pass Type-Safe Argument
In case for move to destination screen while pass some argument and push to the top of the stack.
For the Compose Navigation Library above 2.8.0, We can use Type-Safe Argument and gives us the freedom to determine the type, mandatory, optional, nullable, default value, etc.
For Example like this 2 screen. I want to make navigation like Screen A (pass argument) -> Screen B (receive argument)
:
Screen.kt
sealed interface Screen {
@Serializable
data object A: Screen
@Serializable
data class B(val title: String?, val number: Int): Screen
}
@Composable
fun ScreenA(
onNavBClick: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
onNavBClick()
}
) {
Text("Move to Screen B")
}
}
}
@Composable
fun ScreenB(
title: String?,
number: Int
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Screen B : $title ,$number")
}
}
use this method to get the argument
backStackEntry.toRoute()
MainActivity.kt
NavHost(
...
){
composable<Screen.A>{
ScreenA(
onNavBClick = {
navController.navigate(Screen.B(title = "Message", number = 10))
}
)
}
composable<Screen.B>{ backStackEntry ->
val data: Screen.C = backStackEntry.toRoute()
ScreenC(title = data.title, number = data.number)
}
}
Navigate and Pass Dynamic Argument
In case for move to destination screen while pass some argument like previous case, but not a static data.
For Example like this 2 screen. I want to make navigation like Screen News (pass articleId) -> Screen Article (receive articleId)
with articleId as a dynamic data based on what article that user clicked:
Screen.kt
sealed interface Screen {
@Serializable
data object News: Screen
@Serializable
data class Article(val articleId: String): Screen
}
- use Unit as higher-order functions
action : () -> Unit
- if you want to return some type just add it
action : (String) -> Unit
@Composable
fun NewsScreen(
onArticleClick: (String) -> Unit,
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
onArticleClick("articleId")
}
) {
Text("Move to Screen B")
}
}
}
@Composable
fun ArticleScreen(
articleId: String
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Article : $articleId")
}
}
MainActivity.kt
NavHost(
...
){
composable<Screen.News>{
NewsScreen {
navController.navigate(Screen.Article(it))
}
}
composable<Screen.Article>{ backStackEntry ->
val article: Screen.Article = backStackEntry.toRoute()
ArticleScreen(articleId = article.articleId)
}
}
Back to Previous Screen
The navController.navigateUp()
is a method to back to the previous screen that have same behaviour like system back button. This effectively moves the user back one step in their navigation history. you can set this code to your appbar that have back button.
Scaffold(
topBar = {
TopAppBar(
title = { Text("Back") },
navigationIcon = {
IconButton(onClick = {
navController.navigateUp()
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
){
...
}
There is also some method check if the App still have some stack screen or the only one screen. use the below method to set some behaviour like show/hide back button when app just has 1 stack screen or to show some dialog to make sure the user really want to close the App.
use this method to check previous stack
canNavigateBack = navController.previousBackStackEntry != null
The navController.popBackStack()
method attempts to pop the current destination off the back stack and navigate to the previous destination. This effectively moves the user back one step in their navigation history. It returns a boolean indicating whether it successfully popped back to the destination.
Beside navController.navigateUp()
you can use navController.popBackStack()
to remove or pop current screen ans show previous destination.
Pop Back to a Particular Destination
Unlike the system back button, this type doesn't go back to the previous screen. Instead, it should pop—remove—all screens from the back stack and return to the starting screen.
The popBackStack() method has two required parameters.
navController.popBackStack(route, inclusive)
- route: The string representing the route of the destination you want to navigate back to.
- inclusive = true: Pops (removes) the specified route.
- inclusive = true: popBackStack() will remove all destinations on top of—but not including—the start destination, leaving it as the top most screen visible to the user.
if you just use navController.popBackStack()
, it will be the same behaviour like back button.
But, if you have navigation like Screen A -> Screen B -> Screen C
and use method navController.popBackStack(Screen.A, inclusive = false)
from screen C, it will remove and pop Screen B and Screen C and back to Screen A as the top of stack.
Navigate with PopUpTo a Destination
navController.navigate(Screen.C) {
popUpTo(Screen.A)
}
The method above will Pop everything up to the Screen A destination off the back stack before navigating to the Screen B destination.
For example : navigation Screen A -> Screen B -> Screen C
and you use method above from Screen B, it will pop Screen B and navigate to Screen C. When you back, it will pop Screen C and back to Screen A like this stack Screen A -> Screen C
navController.navigate(Screen.B) {
popUpTo(Screen.A) { inclusive = true }
}
The method above will Pop everything up to and including the Screen A destination off the back stack before navigating to the screen B destination.
- inclusive = true: The specified destination in the popUpTo call, along with all destinations added after it, will be removed from the back stack.
- inclusive = false: Only the destinations that come after the specified destination will be removed, leaving the specified destination itself on the stack.
Launch Single Top
Navigate to the Screen B destination only if we’re not already on the Screen B destination, avoiding multiple copies on the top of the back stack and avoid to show multiple screen when the choosen screen already shown
navController.navigate(Screen.B) {
launchSingleTop = true
}
For example : when you call method navController.navigate(Screen.B)
for two times or more ( accidentally or on purpose ), it will show Screen B as much as you call. When you hit back, it still show Screen B until all Screen B removed.
Navigate Back with Result
For some case, you need to back to previous screen while pass some argument. you can use savedStateHandle
that save some data as state to a key.
For example : you want to get some result or data that save to message
as key of data from Screen B that will pop or back to Screen A.
At Screen B you must get that state and for Screen B to set that state.
- Get the result
navController.currentBackStackEntry?.savedStateHandle?.get<String>("message")
- Set the result
navController.previousBackStackEntry?.savedStateHandle?.set("message", "message from D")
MainActivity.kt
NavHost(
...
){
composable<Screen.A>{
val message =
navController.currentBackStackEntry?.savedStateHandle?.get<String>("message")
ScreenA(
onNavBClick = {
navController.navigate(Screen.B)
},
message = message
)
}
composable<Screen.B>{
ScreenB(
navigateBack = {
navController.previousBackStackEntry?.savedStateHandle?.set("message", "message from D")
navController.popBackStack()
},
)
}
}