Modtion logo
Modtion
android

Android : Handle Navigation for Android Compose

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:

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:

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:

Image
@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):

Image

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
}
@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.

Image
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)

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.

Image

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.

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.

Image

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()
                },
            )
        }
    }