Navigation 구성요소는 Jetpack Compose 애플리케이션을 지원합니다. Navigation 구성요소의 인프라와 기능을 활용하면서 컴포저블 간에 이동할 수 있습니다.
설정
Compose를 지원하려면 앱 모듈의 build.gradle
파일에서 다음 종속 항목을 사용하세요.
Groovy
dependencies { def nav_version = "2.5.3" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.5.3" implementation("androidx.navigation:navigation-compose:$nav_version") }
시작하기
NavController
는 Navigation 구성요소의 중심 API로, 스테이트풀(Stateful)이며 앱의 화면과 각 화면 상태를 구성하는 컴포저블의 백 스택을 추적합니다.
컴포저블에서 rememberNavController()
메서드를 사용하여 NavController
를 만들 수 있습니다.
val navController = rememberNavController()
컴포저블 계층 구조에서 NavController
를 만드는 위치는 이를 참조해야 하는 모든 컴포저블이 액세스할 수 있는 곳이어야 합니다. 이는 상태 호이스팅의 원칙을 준수하며, 이렇게 하면 NavController
와 상태를 사용할 수 있습니다. 상태는 currentBackStackEntryAsState()
를 통해 제공되며 화면 외부에서 컴포저블을 업데이트하기 위한 정보 소스로 사용됩니다. 이 기능의 예를 보려면 하단 탐색 메뉴와 통합을 참고하세요.
NavHost 만들기
각 NavController
를 단일 NavHost
컴포저블과 연결해야 합니다. NavHost
는 구성 가능한 대상을 지정하는 탐색 그래프와 NavController
를 연결합니다. 구성 가능한 대상은 컴포저블 간에 이동할 수 있어야 합니다. 여러 컴포저블 간에 이동하는 과정에서 NavHost
의 콘텐츠가 자동으로 재구성됩니다.
탐색 그래프의 구성 가능한 대상은 각각 경로와 연결됩니다.
NavHost
를 만들려면 이전에 rememberNavController()
를 통해 만든 NavController
뿐만 아니라 그래프의 시작 대상 경로도 필요합니다.
NavHost
를 만드는 데는 탐색 그래프를 생성하는 Navigation Kotlin DSL의 람다 구문이 사용됩니다. composable()
메서드를 사용하여 탐색 구조에 추가할 수 있습니다. 이 메서드를 사용하려면 경로뿐만 아니라 대상에 연결해야 할 컴포저블도 제공해야 합니다.
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
컴포저블로 이동
탐색 그래프에서 구성 가능한 대상으로 이동하려면 navigate
메서드를 사용해야 합니다. navigate
는 대상의 경로를 나타내는 단일 String
매개변수를 사용합니다. 탐색 그래프 내의 컴포저블에서 이동하려면 navigate
를 호출하세요.
navController.navigate("friendslist")
기본적으로 navigate
는 새 대상을 백 스택에 추가합니다. 추가 탐색 옵션을 navigate()
호출에 연결하여 navigate
동작을 수정할 수 있습니다.
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home")
}
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
launchSingleTop = true
}
추가 사용 사례는 popUpTo 가이드를 참고하세요.
다른 구성 가능한 함수로 트리거된 호출 탐색
NavController
의 navigate
함수는 NavController
의 내부 상태를 수정합니다. 단일 정보 소스 원칙을 최대한 준수하려면 NavController
인스턴스를 끌어올리는 구성 가능한 함수나 상태 홀더 및 NavController
를 매개변수로 사용하는 구성 가능한 함수만 탐색 호출을 해야 합니다. UI 계층 구조 아래쪽에 있는 다른 구성 가능한 함수에서 트리거된 탐색 이벤트는 함수를 사용하여 이러한 이벤트를 호출자에게 적절히 노출해야 합니다.
다음 예에서는 구성 가능한 MyAppNavHost
함수를 NavController
인스턴스의 단일 정보 소스로 보여줍니다. ProfileScreen
은 사용자가 버튼을 탭할 때 호출되는 함수로 이벤트를 노출합니다.
앱의 여러 화면으로 이동하는 MyAppNavHost
는 ProfileScreen
을 호출할 때 올바른 대상으로 탐색을 호출합니다.
@Composable
fun MyAppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = "profile"
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("profile") {
ProfileScreen(
onNavigateToFriends = { navController.navigate("friendsList") },
/*...*/
)
}
composable("friendslist") { FriendsListScreen(/*...*/) }
}
}
@Composable
fun ProfileScreen(
onNavigateToFriends: () -> Unit,
/*...*/
) {
/*...*/
Button(onClick = onNavigateToFriends) {
Text(text = "See friends list")
}
}
navigate()
는 컴포저블 자체의 일부가 아닌 콜백의 일부로만 호출하여 모든 재구성에서 navigate()
를 호출하지 않도록 해야 합니다.
권장사항
앱에서 특정 로직을 처리하는 방법을 알고 있는 호출자에게 구성 가능한 함수 이벤트를 노출하는 것은 상태를 끌어올릴 때 Compose에서 사용하기에 좋은 방법입니다.
이벤트를 개별 람다 매개변수로 노출하면 함수 서명이 오버로드될 수 있지만 구성 가능한 함수 책임의 가시성이 극대화됩니다. 함수의 기능을 한눈에 확인할 수 있습니다.
함수 선언에서 매개변수 수를 줄일 수 있는 다른 대안은 처음에는 작성하기에 더 편할 수도 있지만 장기적으로는 몇 가지 단점이 발생하게 됩니다. 예를 들어 ProfileScreenEvents
와 같은 래퍼 클래스를 만들어 모든 이벤트를 한곳에 집중시킬 수 있습니다. 이렇게 하면 컴포저블이 함수 정의를 실행할 때 컴포저블이 어떤 역할을 하는지에 관한 가시성이 줄어듭니다. 또한 프로젝트 수에 다른 클래스와 메서드가 추가되며, 구성 가능한 함수를 호출할 때마다 이러한 클래스의 인스턴스를 만들고 기억해야 합니다. 또한 이 래퍼 클래스를 최대한 재사용하려면 이 패턴의 경우 권장사항인 필요한 부분에만 컴포저블에 전달하는 것이 아닌 UI 계층 구조 아래쪽으로 해당 클래스의 인스턴스를 전달하는 방식이 선호됩니다.
인수를 통해 이동
Navigation Compose는 구성 가능한 대상 간의 인수 전달도 지원합니다. 이렇게 하려면 기본 탐색 라이브러리를 사용할 때 딥 링크에 인수를 추가하는 방법과 유사한 방법으로 인수 자리표시자를 경로에 추가해야 합니다.
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
기본적으로 모든 인수는 문자열로 파싱됩니다. composable()
의 arguments
매개변수는 NamedNavArgument
목록을 허용합니다. navArgument
메서드를 사용하여 신속하게 NamedNavArgument
를 만든 다음 정확한 type
을 지정할 수 있습니다.
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
composable()
함수의 람다에서 사용할 수 있는 NavBackStackEntry
에서 인수를 추출해야 합니다.
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
인수를 대상에 전달하려면 navigate
호출 시 경로에 추가해야 합니다.
navController.navigate("profile/user1234")
지원되는 유형 목록은 대상 간 데이터 전달을 참고하세요.
탐색 시 복잡한 데이터 검색
탐색할 때는 복잡한 데이터 객체를 전달하지 않고 탐색 작업을 실행할 때 고유 식별자 또는 다른 형식의 ID와 같은 필요한 최소 정보를 인수로 전달하는 것이 좋습니다.
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
복잡한 객체는 단일 정보 소스(예: 데이터 영역)에 데이터로 저장해야 합니다. 탐색 후 대상에 도달하면 전달된 ID를 사용하여 단일 정보 소스에서 필요한 정보를 로드할 수 있습니다. 데이터 영역 액세스를 담당하는 ViewModel의 인수를 검색하려면 ViewModel’s
SavedStateHandle
을 사용하면 됩니다.
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)
// …
}
이 접근 방식은 구성 변경 중의 데이터 손실 및 해당 객체가 업데이트되거나 변형될 때 발생하는 불일치를 방지하는 데 도움이 됩니다.
복잡한 데이터를 인수로 전달하지 않아야 하는 이유와 지원되는 인수 유형 목록에 대한 더 자세한 설명은 대상 간 데이터 전달을 참고하세요.
선택적 인수 추가
Navigation Compose는 선택적 탐색 인수도 지원합니다. 선택적 인수는 다음과 같은 두 가지 측면에서 필수 인수와 다릅니다.
- 쿼리 매개변수 문법(
"?argName={argName}"
)을 사용하여 포함해야 합니다. defaultValue
가 설정되어 있거나nullability = true
(암시적으로 기본값을null
로 설정함)가 있어야 합니다.
즉, 모든 선택적 인수는 composable()
함수에 목록 형태로 명확하게 추가해야 합니다.
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
이제 대상에 전달되는 인수가 없더라도 'user1234' defaultValue
가 대신 사용됩니다.
경로를 통해 인수를 처리하는 구조에서는 컴포저블이 Navigation과 완전히 독립적으로 유지되며 테스트 가능성이 훨씬 더 높습니다.
딥 링크
또한 Navigation Compose는 composable()
함수의 일부로 정의할 수 있는 암시적 딥 링크를 지원합니다. deepLinks
매개변수는 navDeepLink
메서드를 사용하여 빠르게 만들 수 있는 NavDeepLink
목록을 허용합니다.
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
이러한 딥 링크를 사용하면 특정 URL, 작업 또는 MIME 유형을 컴포저블과 연결할 수 있습니다. 기본적으로 이러한 딥 링크는 외부 앱에 노출되지 않습니다. 이러한 딥 링크를 외부에서 사용할 수 있도록 하려면 적절한 <intent-filter>
요소를 앱의 manifest.xml
파일에 추가해야 합니다. 위의 딥 링크를 사용 설정하려면 매니페스트의 <activity>
요소 내부에 다음을 추가해야 합니다.
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
다른 앱에서 딥 링크를 트리거하면 Navigation이 딥 링크로 컴포저블과 자동 연결됩니다.
동일한 딥 링크를 사용하면 컴포저블에서 적절한 딥 링크를 통해 PendingIntent
를 빌드할 수도 있습니다.
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
그런 다음 deepLinkPendingIntent
를 다른 PendingIntent
처럼 사용하여 딥 링크 대상에서 앱을 열면 됩니다.
중첩 탐색
대상은 중첩 그래프로 그룹화되어 앱 UI의 특정 흐름을 모듈화할 수 있습니다. 이러한 예는 독립적인 로그인 흐름일 수 있습니다.
중첩 그래프는 대상을 캡슐화합니다. 루트 그래프와 마찬가지로 중첩 그래프에는 경로를 통해 시작 대상으로 식별된 대상이 있어야 합니다. 시작 대상으로 식별된 대상은 사용자가 중첩 그래프와 연결된 경로를 따라 도달하게 되는 곳입니다.
중첩 그래프를 NavHost
에 추가하려면 navigation
확장 함수를 사용하면 됩니다.
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
그래프 크기가 커질수록 탐색 그래프를 여러 메서드로 분할하는 것이 좋습니다. 이렇게 하면 여러 모듈이 자체 탐색 그래프에 기여할 수도 있습니다.
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
이 메서드를 NavGraphBuilder
의 확장 메서드로 만들면 사전 빌드된 navigation
, composable
, dialog
확장 메서드와 함께 사용할 수 있습니다.
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
하단 탐색 메뉴와 통합
하단 탐색 구성요소 등의 다른 구성요소와 Navigation을 연결하려면 컴포저블 계층 구조의 상위 수준에서 NavController
를 정의하면 됩니다. 이렇게 하면 하단 메뉴에서 아이콘을 선택하여 이동할 수 있습니다.
BottomNavigation
및 BottomNavigationItem
구성요소를 사용하려면 Android 애플리케이션에 androidx.compose.material
종속 항목을 추가합니다.
Groovy
dependencies { implementation "androidx.compose.material:material:1.3.1" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.3" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.3.1") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.3" } kotlinOptions { jvmTarget = "1.8" } }
하단 탐색 메뉴에 있는 항목을 탐색 그래프의 경로와 연결하려면 대상 경로 및 문자열 리소스 ID가 포함된 봉인 클래스(여기서는 Screen
)를 정의하는 것이 좋습니다.
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
그런 다음 항목을 BottomNavigationItem
에서 사용할 수 있는 목록에 배치합니다.
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
BottomNavigation
컴포저블에서 currentBackStackEntryAsState()
함수를 사용하여 현재 NavBackStackEntry
를 가져옵니다. 이 항목을 통해 현재 NavDestination
에 액세스할 수 있습니다. 그러면 중첩된 탐색을 사용하고 있는 경우를 처리하도록 NavDestination
계층 구조를 통해 항목의 경로와 현재 대상 및 그 상위 대상의 경로를 비교하여 각 BottomNavigationItem
의 선택된 상태를 확인할 수 있습니다.
항목의 경로는 항목을 탭하여 해당 항목으로 이동하도록 onClick
람다를 navigate
호출에 연결하는 데도 사용됩니다. saveState
및 restoreState
플래그를 사용하면 하단 탐색 항목 간에 전환할 때 항목의 상태와 백 스택이 올바르게 저장되고 복원됩니다.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
여기서는 NavController.currentBackStackEntryAsState()
메서드를 활용하여 NavHost
함수에서 navController
상태를 호이스트하여 BottomNavigation
구성요소와 공유합니다. 즉, BottomNavigation
이 자동으로 최신 상태가 됩니다.
Navigation Compose의 유형 안전성
이 페이지의 코드는 유형에 안전하지 않습니다. 존재하지 않는 경로 또는 잘못된 인수를 사용하여 navigate()
함수를 호출할 수 있습니다. 그러나 런타임 시 유형에 안전하도록 Navigation 코드를 구성할 수 있습니다. 이렇게 하면 비정상 종료를 피하고 다음을 확인할 수 있습니다.
- 대상 또는 탐색 그래프로 이동할 때 제공하는 인수가 올바른 유형이며 모든 필수 인수가 있습니다.
SavedStateHandle
에서 가져오는 인수가 올바른 유형입니다.
자세한 내용은 Navigation 유형 안전성 문서를 참고하세요.
상호 운용성
Compose와 함께 Navigation 구성요소를 사용하려는 경우 다음 두 가지 옵션이 있습니다.
- 프래그먼트의 Navigation 구성요소로 탐색 그래프를 정의합니다.
- Compose 대상을 사용하여 Compose에서
NavHost
로 탐색 그래프를 정의합니다. 탐색 그래프의 모든 화면이 컴포저블인 경우에만 가능합니다.
따라서 혼합 Compose 및 뷰 앱은 프래그먼트 기반의 Navigation 구성요소를 사용하는 것이 좋습니다. 그러면 프래그먼트는 뷰 기반 화면, Compose 화면, 뷰와 Compose를 모두 사용하는 화면을 보유합니다. 각 프래그먼트의 콘텐츠가 Compose에 포함되면 다음 단계는 이러한 화면을 모두 Navigation Compose와 연결하고 모든 프래그먼트를 삭제하는 것입니다.
프래그먼트의 Navigation을 사용하여 Compose에서 이동
Compose 코드 내의 대상을 변경하려면 계층 구조의 컴포저블에 전달되고 컴포저블에서 트리거할 수 있는 이벤트를 노출합니다.
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
프래그먼트에서 NavController
를 찾고 대상으로 이동하여 Compose와 프래그먼트 기반 Navigation 구성요소가 연결되도록 합니다.
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
또는 Compose 계층 구조 아래에 NavController
를 전달해도 됩니다.
그러나 간단한 함수를 노출하면 훨씬 더 쉽게 재사용하고 테스트할 수 있습니다.
테스트
구성 가능한 대상에서 Navigation 코드를 분리하여 NavHost
컴포저블과 별도로 컴포저블을 각각 테스트하는 것을 적극 권장합니다.
즉, navController
를 어떤 컴포저블로도 직접 전달해서는 안 되며 대신 탐색 콜백을 매개변수로 전달해야 합니다. 따라서 테스트에서 navController
인스턴스가 필요하지 않으므로 모든 컴포저블을 개별적으로 테스트할 수 있습니다.
composable
람다에서 제공하는 간접 참조의 수준에 따라 컴포저블 자체에서 Navigation 코드를 분리할 수 있습니다. 이는 다음과 같은 두 방향으로 작동합니다.
- 파싱된 인수만 컴포저블에 전달함
- 컴포저블에서 트리거해야 하는 람다를 전달하여 이동함(
NavController
자체를 전달하지 않음)
예를 들어 userId
를 입력으로 받아들이고 사용자가 친구의 프로필 페이지로 이동할 수 있게 하는 Profile
컴포저블의 경우 서명이 다음과 같을 수 있습니다.
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
이렇게 하면 Profile
컴포저블이 Navigation과 독립적으로 작동하므로 독립적으로 테스트할 수 있습니다. composable
람다는 Navigation API와 컴포저블 간의 격차를 줄이는 데 필요한 최소 논리를 캡슐화합니다.
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
NavHost
, 컴포저블에 전달된 탐색 작업, 개별 화면 컴포저블을 테스트하여 앱 탐색 요구사항을 다루는 테스트를 작성하는 것이 좋습니다.
NavHost
테스트
NavHost
테스트를 시작하려면 다음 탐색 테스트 종속 항목을 추가합니다.
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
NavHost
테스트 제목을 설정하고 navController
인스턴스의 인스턴스를 전달할 수 있습니다. 이를 위해 Navigation 테스트 아티팩트는 TestNavHostController
를 제공합니다. 앱의 시작 대상과 NavHost
를 확인하는 UI 테스트는 다음과 같습니다.
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
탐색 작업 테스트
탐색 구현은 UI 요소를 클릭한 다음 표시되는 대상을 확인하거나 현재 경로를 기준으로 예상 경로를 비교하는 등 여러 가지 방법으로 테스트할 수 있습니다.
앱 구현을 테스트할 때는 UI를 클릭하는 방법을 사용하는 것이 좋습니다. 개별적으로 구성 가능한 함수와 함께 이를 테스트하는 방법을 알아보려면 Jetpack Compose에서 테스트 Codelab을 확인하세요.
또한 navController
의 currentBackStackEntry
를 사용하여 현재 문자열 경로를 예상 경로와 비교함으로써 navController
를 사용하여 어설션을 확인할 수도 있습니다.
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Compose 테스트 기본사항에 관한 자세한 내용은 Compose 테스트 문서 및 Jetpack Compose에서 테스트 Codelab을 참고하세요. 탐색 코드의 고급 테스트에 관한 자세한 내용은 탐색 테스트 가이드를 참고하세요.
자세히 알아보기
Jetpack Navigation에 관해 자세히 알아보려면 Navigation 구성요소 시작하기를 참고하거나 Jetpack Compose Navigation Codelab을 진행하세요.
다양한 화면 크기와 방향, 폼 팩터에 맞게 조정되도록 앱의 탐색을 디자인하는 방법은 반응형 UI용 Navigation을 참고하세요.
중첩된 그래프 및 하단 탐색 메뉴 통합과 같은 개념을 비롯하여 모듈화된 앱의 고급 Navigation Compose 구현에 관한 자세한 내용은 Now in Android 저장소를 참고하세요.