Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

Compose를 통해 이동

탐색 구성요소는 Jetpack Compose 애플리케이션을 지원합니다. 탐색 구성요소의 인프라와 기능을 활용하면서 컴포저블 간에 이동할 수 있습니다.

설정

Compose를 지원하려면 앱 모듈의 build.gradle 파일에서 다음 종속 항목을 사용하세요.

dependencies {
    def nav_compose_version = "1.0.0-alpha01"
    implementation "androidx.navigation:navigation-compose:$nav_compose_version"
}

시작하기

NavController는 탐색 구성요소의 중심 API로, 스테이트풀(Stateful)이며 앱의 화면과 각 화면 상태를 구성하는 컴포저블의 백 스택을 추적합니다.

컴포저블에서 rememberNavController() 메서드를 사용하여 NavController를 만들 수 있습니다.

val navController = rememberNavController()

컴포저블 계층 구조에 참조할 필요가 있는 모든 컴포저블이 액세스하는 계층 구조에 NavController를 만들어야 합니다. 이는 상태 호이스팅의 원칙을 준수하며, 이렇게 하면 NavController와 상태를 사용할 수 있습니다. 상태는 currentBackStackEntryAsState()를 통해 제공되며 화면 외부에서 컴포저블을 업데이트하기 위한 소스 저장소로 사용됩니다. 이 기능의 예를 보려면 하단 탐색 메뉴와 통합을 참고하세요.

NavHost 만들기

NavController를 단일 NavHost 컴포저블과 연결해야 합니다. NavHost는 구성 가능한 대상을 지정하는 탐색 그래프와 NavController를 연결합니다. 구성 가능한 대상은 컴포저블 간에 이동할 수 있어야 합니다. 컴포저블 간에 이동하면 NavHost의 콘텐츠가 자동으로 재구성됩니다. 탐색 그래프의 구성 가능한 대상은 각각 경로와 연결됩니다.

NavHost를 만들려면 이전에 rememberNavController()를 통해 만든 NavController뿐만 아니라 그래프의 시작 대상 경로도 필요합니다. NavHost를 만드는 데는 탐색 그래프를 생성하는 탐색 Kotlin DSL의 람다 구문이 사용됩니다. composable() 메서드를 사용하여 탐색 구조에 추가할 수 있습니다. 이 메서드를 사용하려면 경로뿐만 아니라 대상에 연결해야 할 컴포저블도 제공해야 합니다.

NavHost(navController, startDestination = "profile") {
    composable("profile") { Profile(...) }
    composable("friendslist") { FriendsList(...) }
    ...
}

탐색 그래프에서 구성 가능한 대상으로 이동하려면 navigate() 메서드를 사용해야 합니다. navigate()는 대상의 경로를 나타내는 단일 String 매개변수를 사용합니다. 탐색 그래프 내의 컴포저블에서 이동하려면 navigate()를 호출하세요.

fun Profile(navController: NavController) {
    ...
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    ...
}

navigate()는 컴포저블 자체의 일부가 아닌 콜백의 일부로만 호출하여 모든 재구성에서 navigate()를 호출하지 않도록 해야 합니다.

Navigation Compose는 구성 가능한 대상 간의 인수 전달도 지원합니다. 이렇게 하려면 기본 탐색 라이브러리를 사용할 때 딥 링크에 인수를 추가하는 방법과 유사한 방법으로 인수 자리표시자를 경로에 추가해야 합니다.

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

기본적으로 모든 인수는 문자열로 파싱됩니다. arguments 매개변수를 사용해 type을 설정하면 다른 유형을 지정할 수 있습니다.

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

composable() 함수의 람다에서 사용할 수 있는 NavBackStackEntry에서 NavArguments를 추출해야 합니다.

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

인수를 대상에 전달하려면 navigate 호출 시 자리표시자 대신 경로에 값을 추가해야 합니다.

navController.navigate("profile/user1234")

지원되는 유형 목록은 대상 간 데이터 전달을 참고하세요.

선택적 인수 추가

Navigation Compose는 선택적 탐색 인수도 지원합니다. 선택적 인수는 다음과 같은 두 가지 측면에서 필수 인수와 다릅니다.

  • 쿼리 매개변수 구문("?argName={argName}")을 사용하여 포함해야 합니다.
  • defaultValue가 설정되어 있거나 nullability = true(암시적으로 기본값을 null로 설정함)가 있어야 합니다.

즉, 모든 선택적 인수는 composable() 함수에 목록 형태로 명확하게 추가해야 합니다.

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

이제 대상에 전달되는 인수가 없더라도 'me'의 defaultValue가 대신 사용됩니다.

경로를 통해 인수를 처리하는 구조에서는 컴포저블이 탐색과 완전히 독립적으로 유지되며 테스트 가능성이 훨씬 더 높습니다.

또한 Navigation Compose는 composable() 함수의 일부로 정의할 수 있는 암시적 딥 링크를 지원합니다. navDeepLink()를 사용하여 딥 링크를 목록으로 추가합니다.

val uri = "https://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>

다른 앱에서 딥 링크를 트리거하면 탐색이 딥 링크로 컴포저블과 자동 연결됩니다.

동일한 딥 링크를 사용하면 컴포저블에서 적절한 딥 링크를 통해 PendingIntent를 빌드할 수도 있습니다.

val id = ...
val context = ContextAmbient.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://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처럼 사용하여 딥 링크 대상에서 앱을 열면 됩니다.

하단 탐색 메뉴와 통합

BottomNavBar와 같은 다른 구성요소와 탐색을 연결하려면 컴포저블 계층 구조의 상위 수준에서 NavController를 정의하면 됩니다. 이렇게 하면 하단 메뉴에서 아이콘을 선택하여 이동할 수 있습니다.

하단 탐색 메뉴에 있는 항목을 탐색 그래프의 경로와 연결하려면 대상 경로 및 문자열 리소스 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를 가져오고, 가져온 항목과 NavHostController에 포함된 KEY_ROUTE 상수를 사용하여 인수에서 경로를 검색합니다. 경로를 사용하여 선택한 항목이 현재 대상인지를 확인한 후 라벨을 설정하고 항목을 강조표시하며 경로가 불일치할 때는 이동하면서 적절하게 응답합니다.

val navController = rememberNavController()
Scaffold(
    bottomBar = {
        BottomNavigation {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
            items.forEach { screen ->
                BottomNavigationItem(
                    icon = { Icon(Icons.Filled.Favorite) },
                    label = { Text(stringResource(screen.resourceId)) },
                    selected = currentRoute == screen.route,
                    onClick = {
                        // This is the equivalent to popUpTo the start destination
                        navController.popBackStack(navController.graph.startDestination, false)

                        // This if check gives us a "singleTop" behavior where we do not create a
                        // second instance of the composable if we are already on that destination
                        if (currentRoute != screen.route) {
                            navController.navigate(screen.route)
                        }
                    }
                )
            }
        }
    }
) {

    NavHost(navController, startDestination = Screen.Profile.route) {
        composable(Screen.Profile.route) { Profile(navController) }
        composable(Screen.FriendsList.route) { FriendsList(navController) }
    }
}

여기서는 NavController.currentBackStackEntryAsState() 메서드를 활용하여 NavHost 함수에서 navController 상태를 호이스트하여 BottomNavigation 구성요소와 공유합니다. 즉, BottomNavigation이 자동으로 최신 상태가 됩니다.

테스트

구성 가능한 대상에서 탐색 코드를 분리하여 NavHost 컴포저블과 별도로 컴포저블을 각각 테스트하는 것을 적극 권장합니다.

composable 람다에서 제공하는 간접 참조의 수준에 따라 컴포저블 자체에서 탐색 코드를 분리할 수 있습니다. 이는 작업은 다음과 같은 두 방향으로 작동합니다.

  • 파싱된 인수만 컴포저블에 전달함
  • 컴포저블에서 트리거해야 하는 람다를 전달하여 이동함(NavController 자체를 전달하지 않음)

예를 들어 userId를 입력으로 받아들이고 사용자가 친구의 프로필 페이지로 이동할 수 있게 하는 Profile 컴포저블의 경우 서명이 다음과 같을 수 있습니다.


@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

여기서는 Profile 컴포저블이 탐색과는 독립적으로 작동하므로 독립적으로 테스트할 수 있습니다. composable 람다는 Navigation API와 컴포저블 간의 격차를 줄이는 데 필요한 최소 논리를 캡슐화합니다.

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
}

자세히 알아보기

Jetpack 탐색에 관한 자세한 내용은 탐색 구성요소 시작하기를 참고하세요.