Kotlin DSL을 사용하여 프로그래매틱 방식으로 그래프 빌드

탐색 구성요소는 Kotlin 기반의 도메인별 언어인 DSL을 제공하며, DSL은 Kotlin의 유형 안전 빌더를 사용합니다. 이 API를 사용하면 XML 리소스 내부가 아니라 Kotlin 코드에서 그래프를 선언적으로 구성할 수 있습니다. 앱의 탐색을 동적으로 빌드하고자 할 때 유용합니다. 예를 들어 앱에서 외부 웹 서비스의 탐색 구성을 다운로드 및 캐시한 다음, 이 구성을 사용해 활동의 onCreate() 함수에서 탐색 그래프를 동적으로 빌드할 수 있습니다.

종속 항목

Kotlin DSL을 사용하려면 앱의 build.gradle 파일에 다음 종속 항목을 추가하세요.

Groovy

dependencies {
    def nav_version = "2.7.7"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

그래프 빌드

Sunflower 앱을 기반으로 기본적인 예시부터 살펴보겠습니다. 이 예시에는 homeplant_detail이라는 두 개의 대상이 있습니다. 사용자가 처음으로 앱을 실행하면 home 대상이 표시됩니다. 이 대상은 사용자의 정원에 있는 식물 목록을 표시합니다. 사용자가 식물 중 하나를 선택하면 앱이 plant_detail 대상으로 이동합니다.

그림 1은 plant_detail 대상에서 요구하는 인수와 앱이 home에서 plant_detail로 이동할 때 사용하는 작업인 to_plant_detail을 함께 보여줍니다.

Sunflower 앱에는 두 개의 대상과 두 대상을 연결하는 작업이 있습니다.
그림 1. Sunflower 앱에는 homeplant_detail이라는 두 개의 대상과 두 대상을 연결하는 작업이 있습니다.

Kotlin DSL 탐색 그래프 호스팅

앱의 탐색 그래프를 빌드하려면 그래프를 호스팅할 위치가 필요합니다. 이 예에서는 앱이 FragmentContainerView 내부에 있는 NavHostFragment에 그래프를 호스팅하도록 프래그먼트를 사용합니다.

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

참고로, 이 예에서는 app:navGraph 속성이 설정되어 있지 않습니다. 그래프가 res/navigation 폴더에 리소스로 정의되어 있지 않으므로, 활동에서 그래프를 onCreate() 프로세스의 일부로 설정해야 합니다.

XML에서 작업은 하나 이상의 인수가 있는 대상 ID를 서로 연결합니다. 그러나 Navigation DSL을 사용하는 경우 경로는 경로의 일부로 인수를 포함할 수 있습니다. 즉, DSL을 사용할 때는 작업에 대한 개념이 없습니다.

다음은 그래프를 정의할 때 사용할 상수를 정의하는 단계입니다.

그래프 상수 만들기

XML 기반 탐색 그래프는 Android 빌드 프로세스의 일부로 파싱됩니다. 그래프에 정의된 id 속성마다 숫자 상수가 생성됩니다. 이러한 빌드 시간 생성 정적 ID는 런타임에 탐색 그래프를 빌드할 때 사용할 수 없으므로 Navigation DSL은 ID 대신 경로 문자열을 사용합니다. 각 경로는 고유한 문자열로 표현되며, 오타와 관련된 버그 위험을 줄이기 위해 이러한 경로를 상수로 정의하는 것이 좋습니다.

인수를 처리할 때 인수는 경로 문자열에 포함됩니다. 다시 한 번 언급하자면, 이 로직을 경로에 빌드하면 오타 관련 버그가 유입될 위험을 줄일 수 있습니다.

object nav_routes {
    const val home = "home"
    const val plant_detail = "plant_detail"
}

object nav_arguments {
    const val plant_id = "plant_id"
    const val plant_name = "plant_name"
}

상수를 정의한 후에는 탐색 그래프를 빌드할 수 있습니다.

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = nav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

이 예에서 후행 람다는 fragment() DSL 빌더 함수를 사용하여 두 개의 프래그먼트 대상을 정의합니다. 이 함수에는 상수에서 가져온 대상의 경로 문자열이 필요합니다. 이 함수는 대상 라벨과 같이 추가 구성에 사용할 람다(선택사항)와 인수와 딥 링크에 사용할 삽입된 빌더 함수도 허용합니다.

각 대상의 UI를 관리하는 Fragment 클래스는 매개변수화된 유형(<>)으로 꺾쇠괄호 안에 포함됩니다. 이는 XML을 사용해 정의된 프래그먼트 대상에 android:name 속성을 설정하는 것과 동일한 효과가 있습니다.

마지막으로 표준 NavController.navigation() 호출을 사용하여 home에서 plant_detail로 이동할 수 있습니다.

private fun navigateToPlant(plantId: String) {
   findNavController().navigate("${nav_routes.plant_detail}/$plantId")
}

PlantDetailFragment에서 다음 예와 같이 인수 값을 가져올 수 있습니다.

val plantId: String? = arguments?.getString(nav_arguments.plant_id)

탐색 시 인수를 제공하는 방법에 관한 자세한 내용은 대상 인수 제공 섹션을 참고하세요.

이 가이드의 나머지 부분에서는 일반적인 탐색 그래프 요소, 대상, 및 그래프 빌드 시 이러한 요소를 사용하는 방법을 설명합니다.

대상

Kotlin DSL은 세 가지 대상 유형을 기본적으로 지원하며, Fragment 대상, Activity 대상, NavGraph 대상이 이에 해당합니다. 각 항목에는 해당 대상을 빌드하고 구성하는 데 사용할 수 있는 자체 인라인 확장 함수가 있습니다.

Fragment 대상

fragment() DSL 함수는 구현하는 프래그먼트 클래스에 매개변수화될 수 있으며, 이 대상에 할당할 고유한 경로 문자열(이 다음에는 Kotlin DSL 그래프로 탐색 섹션에서 설명한 대로 추가 구성을 제공할 수 있는 람다가 옴)을 취합니다.

fragment<FragmentDestination>(nav_routes.route_name) {
   label = getString(R.string.fragment_title)
   // arguments, deepLinks
}

Activity 대상

activity() DSL 함수는 대상에 할당할 고유 경로 문자열을 사용하지만, 구현하는 활동 클래스에 매개변수화되지는 않습니다. 대신 후행 람다에서 선택적으로 activityClass를 설정할 수 있습니다. 이러한 유연성 덕분에 암시적 인텐트를 사용하여 시작되어야 하는 활동의 활동 대상을 정의할 수 있으며, 이때 명시적 활동 클래스는 적합하지 않습니다. 프래그먼트 대상과 마찬가지로 라벨, 인수, 딥 링크도 구성할 수 있습니다.

activity(nav_routes.route_name) {
   label = getString(R.string.activity_title)
   // arguments, deepLinks...

   activityClass = ActivityDestination::class
}

navigation() DSL 함수를 사용하여 중첩된 탐색 그래프를 빌드할 수 있습니다. 이 함수는 세 가지 인수를 취하며, 그래프에 할당할 경로, 그래프의 시작 대상 경로, 그래프 추가 구성을 위한 람다가 이에 해당합니다. 유효한 요소에는 다른 대상, 인수, 딥 링크를 비롯해 대상에 대한 설명 라벨이 포함됩니다. 이 라벨은 NavigationUI를 사용하여 탐색 그래프를 UI 구성요소에 결합하는 데 유용할 수 있습니다.

navigation("route_to_this_graph", nav_routes.home) {
   // label, other destinations, deep links
}

맞춤 대상 지원

사용 중인 새 대상 유형이 Kotlin DSL을 직접 지원하지 않는 경우 다음과 같이 addDestination()을 사용하여 그러한 대상을 Kotlin DSL에 추가할 수 있습니다.

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

또는 단항 플러스 연산자를 사용하여 새로 구성된 대상을 그래프에 직접 추가할 수도 있습니다.

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

대상 인수 제공

모든 대상은 선택적 인수 또는 필수 인수를 정의할 수 있습니다. 작업은 모든 대상 빌더 유형의 기본 클래스인 NavDestinationBuilder에서 argument() 함수를 사용하여 정의할 수 있습니다. 이 함수는 인수의 이름을 문자열로 취하고 NavArgument를 생성 및 구성하는 데 사용되는 람다를 취합니다.

람다 내에서 인수 데이터 유형, 기본값(해당하는 경우), null 허용 여부를 지정할 수 있습니다.

fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
    label = getString(R.string.plant_details_title)
    argument(nav_arguments.plant_id) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_id)
        nullable = true  // default false
    }
}

defaultValue가 주어지면 유형을 추론할 수 있습니다. defaultValuetype이 모두 주어지는 경우 유형이 일치해야 합니다. 사용 가능한 인수 유형의 전체 목록은 NavType 참조 문서를 확인하세요.

맞춤 유형 제공

ParcelableTypeSerializableType과 같은 특정 유형은 경로 또는 딥 링크에 사용되는 문자열에서 값을 파싱하는 것을 지원하지 않습니다. 이는 이러한 유형이 런타임 시 리플렉션에 의존하지 않기 때문입니다. 맞춤 NavType 클래스를 제공하여, 경로 또는 딥 링크에서 유형을 파싱하는 방식을 정확하게 제어할 수 있습니다. 이렇게 하면 Kotlin 직렬화 또는 다른 라이브러리를 사용하여 맞춤 유형을 리플렉션 없이 인코딩 및 디코딩할 수 있습니다.

예를 들어 검색 화면에 전달되는 검색 매개변수를 나타내는 데이터 클래스는 Serializable(인코딩/디코딩을 지원하기 위한 용도)과 Parcelize(Bundle에 저장하고 Bundle에서 복원하기 위한 용도)를 모두 구현할 수 있습니다.

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

맞춤 NavType을 다음과 같이 작성할 수 있습니다.

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

  // Only required when using Navigation 2.4.0-alpha07 and lower
  override val name = "SearchParameters"
}

그런 다음 이를 다른 유형과 마찬가지로 Kotlin DSL에서 사용할 수 있습니다.

fragment<SearchFragment>(nav_routes.plant_search) {
    label = getString(R.string.plant_search_title)
    argument(nav_arguments.search_parameters) {
        type = SearchParametersType
        defaultValue = SearchParameters("cactus", emptyList())
    }
}

이 예에서는 Kotlin 직렬화를 사용하여 문자열의 값을 파싱합니다. 즉, 대상으로 이동할 때 형식이 일치하는지 확인하려면 Kotlin 직렬화도 사용해야 합니다.

val params = SearchParameters("rose", listOf("available"))
val searchArgument = Uri.encode(Json.encodeToString(params))
navController.navigate("${nav_routes.plant_search}/$searchArgument")

매개변수는 대상의 인수에서 가져올 수 있습니다.

val params: SearchParameters? = arguments?.getParcelable(nav_arguments.search_parameters)

딥 링크

딥 링크는 XML 기반 탐색 그래프와 함께 추가될 수 있는 것처럼 모든 대상에 추가 가능합니다. 대상의 딥 링크 만들기에서 정의된 것과 동일한 모든 절차가 Kotlin DSL을 사용하는 명시적 딥 링크 만들기 프로세스에도 적용됩니다.

하지만 암시적 딥 링크를 만들 때는 <deepLink> 요소에 대해 분석할 수 있는 XML 탐색 리소스가 없습니다. 따라서 AndroidManifest.xml 파일에 <nav-graph> 요소를 배치할 수 없으므로 활동에 수동으로 인텐트 필터를 추가해야 합니다. 제공하는 인텐트 필터는 앱 딥 링크의 기본 URL 패턴과 작업 및 MIME 유형과 일치해야 합니다.

deepLink() DSL 함수를 사용하여 개별적으로 딥 링크로 연결된 각 대상에 더 구체적인 deeplink를 제공할 수 있습니다. 이 함수는 NavDeepLink를 허용합니다. 이 요소는 URI 패턴을 나타내는 String, 인텐트 작업을 나타내는 String, mimeType을 나타내는 String을 포함합니다.

예:

deepLink {
    uriPattern = "http://www.example.com/plants/"
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
}

추가할 수 있는 딥 링크 수에는 제한이 없습니다. deepLink()를 호출할 때마다 해당 대상에 대해 유지되는 목록에 새로운 딥 링크가 추가됩니다.

아래는 경로 및 쿼리 기반 매개변수도 정의하는 좀 더 복잡한 암시적 딥 링크 시나리오입니다.

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_routes.plant_detail) {
   label = getString(R.string.plant_details_title)
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}"
   })
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}?name={plant_name}"
   })
}

문자열 보간 유형을 사용하여 정의를 간소화할 수 있습니다.

제한사항

Safe Args 플러그인은 Kotlin DSL과 호환되지 않습니다. 플러그인이 DirectionsArguments 클래스를 생성하기 위해 XML 리소스 파일을 찾기 때문입니다.

자세히 알아보기

탐색 유형 안전성 페이지에서 Kotlin DSL 및 Navigation Compose 코드에 유형 안전성을 제공하는 방법을 알아보세요.