Destination 들이 포함된 UI 컴포넌트. 사용자가 앱 내부에서 Navigating을 할 때 앱은 기본적으로 NavHost 를 통해서 화면 간 전환을 구현합니다. NavHost 의 builder 람다블록 파라미터를 통해 destination 들을 추가할 수 있고, navController 를 통해 화면 전환 기능을 할 수 있습니다.
public fun NavHost(
navController: NavHostController,
startDestination: Any,
...
enterTransition: ...,
exitTransition: ...,
popEnterTransition: ... = enterTransition,
popExitTransition: ... = exitTransition,
sizeTransform: ... = null,
builder: NavGraphBuilder.() -> Unit
) {
NavHost(
navController,
remember(route, startDestination, builder) {
navController.createGraph(startDestination, route, typeMap, builder)
},
...
)
}
여러 Transition 을 적용해서 화면 이동 애니메이션을 구현하고, 커스텀 가능합니다.
navController.createGraph() 함수를 사용해서 remember 되는 NavGraph 데이터를 생성하고, navController.graph 에 할당합니다.
public fun NavHost(
...
graph: NavGraph, // remember(...) { navController.createGraph(...) }
...
) {
...
// Then set the graph
navController.graph = graph
앱 내의 모든 NavDestination 과 연결 방법들이 정의된 데이터 구조 입니다. 아래 사진과 같이 트리 구조로 되어 있고, 탐색 시엔 DFS(Depth-First-Search) 방식으로 트리를 순회합니다. NavGrpah 안에 NavGrpah 가 존재할 수 있으며, 이를 Nested NavGraph 라고 합니다.

NavHost 에 있는 navController.createGraph() 함수에서 NavGraphBuilder(…).apply(builder).build() 함수를 리턴하는데, 이때 마지막 build() 함수에서 그래프에 destination 들이 추가됩니다.
// NavGraphBuilderkt
override fun build(): NavGraph =
super.build().also { navGraph ->
navGraph.addDestinations(destinations)
...
}
}
NavDestination 들을 관리하고 네비게이팅하는 중앙 코디네이터입니다. NavDestination 간 탐색, Deep Link 처리, BackStack 관리 등의 작업을 위한 여러가지 메소드를 제공합니다. NavHost 에서 생성한 NavGraph 를 컨트롤러의 graph 에 할당합니다.
navController.graph = graph
NavGraph 의 하나의 노드에 해당합니다. Navigator.createDestination 함수를 통해 생성됩니다.
앱이 이 노드로 이동하면 NavHost 가 현재 NavDestination 에 해당하는 콘텐츠(화면)을 표시합니다. 각 목적지는 arguments set 을 가질 수 있고, 네비게이팅을 하면서 default value 를 오버라이드해서 할당할 수 있습니다.
NavDestination 이 고유하게 가지는 식별값입니다. 이 Route 를 통해서 Navigator 가 특정 NavDestination 으로 네비게이팅이 가능합니다.
직렬화 가능한 모든 데이터 유형이 대상이 될 수 있으며, String 뿐만 아니라 @Serializable 한 모든 객체를 사용할 수 있습니다.
애니메이션
fadeIn , fadeOut 을 기본적으로 제공하고, 필요에 따라 파라미터를 넘겨주는 방식으로 간단하게 커스텀할 수 있습니다.Deep Link
NavDestination 으로 이동시킬 수 있습니다.여러 UI 에 적용 가능
Type Safety
@Serializable 한 객체를 경로로 설정할 수 있어 컴파일 단계에서 경로 설정 오류를 잡을 수 있으며, 객체의 생성자 파라미터를 통해서 데이터를 안전하게 주고받을 수 있습니다. (이 또한 컴파일 단계에서 오류를 잡을 수 있습니다.)ViewModel
Back Handler
plugins {
kotlin("plugin.serialization") version "2.0.21" // Serialization
}
dependencies {
val nav_version = "2.8.9"
// Jetpack Compose integration
implementation("androidx.navigation:navigation-compose:$nav_version")
// Testing Navigation
androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
// JSON serialization library, works with the Kotlin serialization plugin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
val navController: NavHostController = rememberNavController()
이 navController 인스턴스는 NavHost(navController = navController, …) 에 넘겨줄 것이기 때문에 NavHost 보다 높은 위치에서 만들어야 합니다.
Route 는 직렬화 가능한 데이터 형식을 사용할 수 있습니다. 간단하게 String 으로 (route = "home") 넣어줄 수 있지만, Type Safety 를 위해 object 나 class 에 @Serializable 어노테이션을 붙여서 직렬화 하는 것을 추천합니다.
@Serializable
object Profile
@Serializable
object FriendsLists
인자에 navController 와 startDestination 을 넘겨줍니다.
NavHost(navController = navController, startDestination = Profile) {
composable<Profile> { ProfileScreen( /* ... */ ) }
composable<FriendsList> { FriendsListScreen( /* ... */ ) }
}
잘 쓰이진 않지만 내부 개념을 이해하면 다음과 같이도 만들 수 있습니다. (밑에서 기술)
val navGraph by remember(navController) {
navController.createGraph(startDestination = Profile)) {
composable<Profile> { ProfileScreen( /* ... */ ) }
composable<FriendsList> { FriendsListScreen( /* ... */ ) }
}
}
NavHost(navController, navGraph)
기본 화면 이동 방법
val navController = rememberNavController()
navController.navigate(route = Home)
NavHost 와 같이 쓰기
화면 ↔ 화면 간 네비게이팅은, 시작화면과 도착화면이 존재합니다.
시작화면에 navController.navigate(route = {도착화면경로}) 를 인자로 넘겨줍니다.
SimpleHomeRoute(
navigateToProfile = { navController.navigate(route = Profile) }
...
)
navController 를 넘겨서 화면 안에서 네비게이팅 기능을 정의해도 되지만, 이는 권장되지 않습니다. UDF(단방향 데이터 흐름) 에 어긋나기 때문입니다. 그래서 State 인 navController 를 상위로 올려 State Hoisting 을 구현합니다.
참고 : UDF(Unidirectional Data Flow)
https://developer.android.com/topic/architecture/ui-layer?hl=ko#udf
전체 코드
NavHost(navController = navController, startDestination = Simple.Home) {
composable<Simple.Home> {
SimpleHomeRoute(
padding = padding,
navigateToProfile = {
navController.navigate(route = Simple.Profile(it)) {
popUpTo(route = Simple.Home) { inclusive = true }
}
})
}
composable<Simple.Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Simple.Profile>()
SimpleProfileRoute(
padding = padding,
name = profile.name,
navigateToHome = { navController.navigate(Simple.Home) }
)
}
}
딥 링크
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
딥 링크(Deep Link) 란, 앱에서 특정한 콘텐츠나 화면으로 직접 연결해주는 기술입니다.
사용자를 특정 앱으로 이동시켜서 원하는 화면을 보여줍니다.
참고 : https://brunch.co.kr/@144ce42dff304dd/14
Nested NavGraph
NavHost 는 builder 람다 블록에 있는 NavGraphBuilder 의 확장함수들을 NavGraph 로 만듭니다. NavGraph 의 하위에 NavGraph 가 존재할 수 있고 이를 Nested NavGraph(중첩 그래프) 라고 합니다.
composable() , navigation() , dialog() 모두 NavGraphBuilder 의 확장함수 입니다.
NavHost(navController, startDestination = Title) {
composable<Title> {
TitleScreen(...)
}
navigation<Game>(startDestination = Match) {
composable<Match> {
MatchScreen(..)
}
composable<InGame> {
InGameScreen(...)
}
}
}
Navigation Code 캡슐화
중첩된 그래프가 많아거나, 네비게이팅할 화면이 많아진다면, NavHost 의 builder 람다 블록의 코드들이 매우 길어지고 유지보수하기 어려워질 수 있습니다.
이 때 Nested NavGraph 부분들 따로 커스텀해서 캡슐화해 코드의 간결성과 유지보수성을 챙길 수 있습니다.
NavHost(navController, startDestination = Title) {
...
navigation<Game>(startDestination = Contact) {
composable<ContactDetails> {
ContactDetailsScreen(
navigateToContact = navigateToContact
)
}
composable<Contact> {
ContactScreen(
navigateToContactDetail = navigateToContactDetail(it)
)
}
}
...
}
// ContactNavGraph.kt
fun NavGraphBuilder.contactNavGraph(
navigateToContact : () -> Unit,
navigateToContactDetails : () -> Unit
) {
composable<ContactDetails> { navBackStackEntry ->
ContactDetailsScreen(
contact = navBackStackEntry.toRoute(),
navigateToContact = navigateToContact
)
}
composable<Contact> {
ContactScreen(
navigateToContactDetail = navigateToContactDetail(it)
)
}
}
NavHost(navController, startDestination = Title) {
...
// navigation<Game>(startDestination = Contact) {
// composable<ContactDetails> {
// ContactDetailsScreen(
// navigateToContact = navigateToContact
// )
// }
// composable<Contact> {
// ContactScreen(
// navigateToContactDetail = navigateToContactDetail
// )
// }
// }
contactNavGraph(
navigateToContact : { navController.navigate(route = Contact) },
navigateToContactDetails : { navController.navigate(route = ContactDetail) }
...
}
네비게이팅을 할 때 추가적으로 설정할 수 있는 옵션들입니다.
참고 : https://developer.android.com/reference/androidx/navigation/NavOptions
- popUpTo(route) : route 까지 백스택을 모두 제거함
- inclusive : popUpTo 와 같이 쓰이며, true 일 경우 popUpTo(route) 의 route 도 같이 백스택에서 제거함
- launchSingleTop : 동일한 destination 이 중복되게 스택에 쌓이는 것을 방지함
- 만약 같은 destination 이 backStack 에 있다면 그 destination 을 지움.
- 기본적으로 최상단에 쌓이기 때문에 singleTop 이 됨.
- restoreState : 다른 화면에 갔다가 다시 돌아왔을 때 이전 상태를 유지함.
- 보통 BottomNavigation 에서 쓰임