[Android/Compose] 컴포즈 네비게이션 다루기

Kunnam·2025년 3월 22일

드림학기

목록 보기
3/7

Compose-Navigation

주요 개념

  • NavHost
  • NavGraph
  • NavController
  • NavDestination
  • Route

Destination 들이 포함된 UI 컴포넌트. 사용자가 앱 내부에서 Navigating을 할 때 앱은 기본적으로 NavHost 를 통해서 화면 간 전환을 구현합니다. NavHostbuilder 람다블록 파라미터를 통해 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 를 오버라이드해서 할당할 수 있습니다.

Route

NavDestination 이 고유하게 가지는 식별값입니다. 이 Route 를 통해서 Navigator 가 특정 NavDestination 으로 네비게이팅이 가능합니다.

직렬화 가능한 모든 데이터 유형이 대상이 될 수 있으며, String 뿐만 아니라 @Serializable 한 모든 객체를 사용할 수 있습니다.

장점 및 기능

애니메이션

  • 화면 간 이동을 할 시에 애니메이션 효과를 제공합니다. fadeIn , fadeOut 을 기본적으로 제공하고, 필요에 따라 파라미터를 넘겨주는 방식으로 간단하게 커스텀할 수 있습니다.

Deep Link

  • 내부적으로 deep link 를 다루고 구현하고 있기 때문에 간편하게 사용자를 startDestination 이 아닌 NavDestination 으로 이동시킬 수 있습니다.

여러 UI 에 적용 가능

  • 화면 전환을 사용하는 여러 UI 패턴(Drawer, Bottom Nav 등) 에 최소한에 작업으로 네비게이팅 기능을 삽입할 수 있습니다.

Type Safety

  • Kotlin Serialization 을 통한 Route 를 지원합니다. @Serializable 한 객체를 경로로 설정할 수 있어 컴파일 단계에서 경로 설정 오류를 잡을 수 있으며, 객체의 생성자 파라미터를 통해서 데이터를 안전하게 주고받을 수 있습니다. (이 또한 컴파일 단계에서 오류를 잡을 수 있습니다.)

ViewModel

  • ViewModel 의 스코프를 Navigation Graph 로 지정해서 NavDestination 간에 데이터를 공유할 수 있습니다.
  • 그 외 Jetpack 라이브러리들과도 상호작용을 할 수 있습니다.

Back Handler

  • 핸드폰의 기본 뒤로가기 버튼을 내부적으로 처리해 일반적인 사용자 경험(UX) 를 유지할 수 있습니다.

사용법

build.gradle.kts

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 보다 높은 위치에서 만들어야 합니다.

  1. Route 설정

Route 는 직렬화 가능한 데이터 형식을 사용할 수 있습니다. 간단하게 String 으로 (route = "home") 넣어줄 수 있지만, Type Safety 를 위해 objectclass@Serializable 어노테이션을 붙여서 직렬화 하는 것을 추천합니다.

@Serializable
object Profile
@Serializable
object FriendsLists
  1. NavHost 선언

인자에 navControllerstartDestination 을 넘겨줍니다.

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 캡슐화

중첩된 그래프가 많아거나, 네비게이팅할 화면이 많아진다면, NavHostbuilder 람다 블록의 코드들이 매우 길어지고 유지보수하기 어려워질 수 있습니다.

이 때 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 에서 쓰임
profile
공부블로그

0개의 댓글