Navigation 구성요소는 Jetpack Compose 애플리케이션을 지원합니다. 탐색 구성요소의 인프라와 기능을 활용하면서 컴포저블 간에 이동할 수 있습니다.
설정
Compose를 지원하려면 앱 모듈의 build.gradle
파일에서 다음 종속 항목을 사용하세요.
Groovy
dependencies { def nav_version = "2.8.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
시작하기
앱에서 탐색을 구현할 때는 탐색 호스트, 그래프, 컨트롤러를 구현합니다. 자세한 내용은 탐색 개요를 참고하세요.
NavController 만들기
Compose에서 NavController
를 만드는 방법에 관한 자세한 내용은 탐색 컨트롤러 만들기의 Compose 섹션을 참고하세요.
NavHost 만들기
Compose에서 NavHost
를 만드는 방법에 관한 자세한 내용은 탐색 그래프 설계의 Compose 섹션을 참고하세요.
컴포저블로 이동
컴포저블로 이동하는 방법에 관한 자세한 내용은 아키텍처 문서의 대상으로 이동을 참고하세요.
인수를 통해 이동
컴포저블 대상 간에 인수를 전달하는 방법에 관한 자세한 내용은 탐색 그래프 설계의 Compose 섹션을 참고하세요.
탐색 시 복잡한 데이터 검색
탐색할 때는 복잡한 데이터 객체를 전달하지 않고 탐색 작업을 실행할 때 고유 식별자 또는 다른 형식의 ID와 같은 필요한 최소 정보를 인수로 전달하는 것이 좋습니다.
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
복잡한 객체는 단일 정보 소스(예: 데이터 영역)에 데이터로 저장해야 합니다. 탐색 후 대상에 도달하면 전달된 ID를 사용하여 단일 정보 소스에서 필요한 정보를 로드할 수 있습니다. 데이터 영역 액세스를 담당하는 ViewModel
의 인수를 검색하려면 다음과 같이 ViewModel
의 SavedStateHandle
를 사용합니다.
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
이 접근 방식은 구성 변경 중의 데이터 손실 및 해당 객체가 업데이트되거나 변형될 때 발생하는 불일치를 방지하는 데 도움이 됩니다.
복잡한 데이터를 인수로 전달하지 않아야 하는 이유와 지원되는 인수 유형 목록에 대한 더 자세한 설명은 대상 간 데이터 전달을 참고하세요.
딥 링크
또한 Navigation Compose는 composable()
함수의 일부로 정의할 수 있는 딥 링크를 지원합니다. deepLinks
매개변수는 navDeepLink()
메서드를 사용하여 빠르게 만들 수 있는 NavDeepLink
객체 목록을 허용합니다.
@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)
}
이러한 딥 링크를 사용하면 특정 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/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
그런 다음 deepLinkPendingIntent
를 다른 PendingIntent
처럼 사용하여 딥 링크 대상에서 앱을 열면 됩니다.
중첩 탐색
중첩된 탐색 그래프를 만드는 방법에 관한 자세한 내용은 중첩 그래프를 참고하세요.
하단 탐색 메뉴와 통합
하단 탐색 구성요소 등의 다른 구성요소와 Navigation을 연결하려면 컴포저블 계층 구조의 상위 수준에서 NavController
를 정의하면 됩니다. 이렇게 하면 하단 메뉴에서 아이콘을 선택하여 이동할 수 있습니다.
BottomNavigation
및 BottomNavigationItem
구성요소를 사용하려면 Android 애플리케이션에 androidx.compose.material
종속 항목을 추가합니다.
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
하단 탐색 메뉴에 있는 항목을 탐색 그래프의 경로와 연결하려면 경로 클래스와 아이콘이 있는 클래스(여기서는 TopLevelRoute
)를 정의하는 것이 좋습니다.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
그런 다음 해당 경로를 BottomNavigationItem
에서 사용할 수 있는 목록에 배치합니다.
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
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
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
여기서는 NavController.currentBackStackEntryAsState()
메서드를 활용하여 NavHost
함수에서 navController
상태를 호이스트하여 BottomNavigation
구성요소와 공유합니다. 즉, BottomNavigation
이 자동으로 최신 상태가 됩니다.
상호 운용성
Compose와 함께 Navigation 구성요소를 사용하려는 경우 다음 두 가지 옵션이 있습니다.
- 프래그먼트의 Navigation 구성요소로 탐색 그래프를 정의합니다.
- Compose 대상을 사용하여 Compose에서
NavHost
로 탐색 그래프를 정의합니다. 탐색 그래프의 모든 화면이 컴포저블인 경우에만 가능합니다.
따라서 혼합 Compose 및 뷰 앱은 프래그먼트 기반의 Navigation 구성요소를 사용하는 것이 좋습니다. 그러면 프래그먼트는 뷰 기반 화면, Compose 화면, 뷰와 Compose를 모두 사용하는 화면을 보유합니다. 각 프래그먼트의 콘텐츠가 Compose에 포함되면 다음 단계는 이러한 화면을 모두 Navigation Compose와 연결하고 모든 프래그먼트를 삭제하는 것입니다.
프래그먼트의 Navigation을 사용하여 Compose에서 이동
Compose 코드 내의 대상을 변경하려면 계층 구조의 컴포저블에 전달되고 컴포저블에서 트리거할 수 있는 이벤트를 노출합니다.
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
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
를 입력으로 받아들이고 사용자가 친구의 프로필 페이지로 이동할 수 있게 하는 ProfileScreen
컴포저블의 경우 서명이 다음과 같을 수 있습니다.
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
이렇게 하면 ProfileScreen
컴포저블이 Navigation과 독립적으로 작동하므로 독립적으로 테스트할 수 있습니다. composable
람다는 Navigation API와 컴포저블 간의 격차를 줄이는 데 필요한 최소 논리를 캡슐화합니다.
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
NavHost
, 컴포저블에 전달된 탐색 작업, 개별 화면 컴포저블을 테스트하여 앱 탐색 요구사항을 다루는 테스트를 작성하는 것이 좋습니다.
NavHost
테스트
NavHost
테스트를 시작하려면 다음 탐색 테스트 종속 항목을 추가합니다.
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
NavHostController
를 매개변수로 허용하는 컴포저블에 앱의 NavHost
를 래핑합니다.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
이제 탐색 테스트 아티팩트 TestNavHostController
의 인스턴스를 전달하여 AppNavHost
및 NavHost
내에 정의된 모든 탐색 로직을 테스트할 수 있습니다. 앱의 시작 대상과 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()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Compose 테스트 기본사항에 관한 자세한 내용은 Compose 레이아웃 테스트 및 Jetpack Compose에서 테스트 Codelab을 참고하세요. 탐색 코드의 고급 테스트에 관한 자세한 내용은 탐색 테스트 가이드를 참고하세요.
자세히 알아보기
Jetpack Navigation에 관해 자세히 알아보려면 Navigation 구성요소 시작하기를 참고하거나 Jetpack Compose Navigation Codelab을 진행하세요.
다양한 화면 크기와 방향, 폼 팩터에 맞게 조정되도록 앱의 탐색을 디자인하는 방법은 반응형 UI 탐색을 참고하세요.
중첩된 그래프 및 하단 탐색 메뉴 통합과 같은 개념을 비롯하여 모듈화된 앱의 고급 Compose 탐색 구현에 관한 자세한 내용은 GitHub의 Now in Android 앱을 참고하세요.
샘플
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- Compose의 Material Design 2
- Jetpack Navigation을 Navigation Compose로 이전
- 상태를 호이스팅할 위치