使用 Kotlin DSL 透過程式化方式建構圖表

導覽元件提供以 Kotlin 為基礎的網域特定語言 (即 DSL),而且必須依賴 Kotlin 的類型安全建構工具來建立資料結構。這個 API 可讓您在 Kotlin 程式碼中 (而非 XML 資源內) 透過宣告製作圖表。如果您想動態建構應用程式的導覽機制,這項功能就能派上用場。例如,應用程式可從外部網路服務下載及快取導覽設定,然後使用該設定在活動的 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 和一個或多個引數連結在一起。不過,使用導覽 DSL 時,路徑可以包含引數,當做路徑的一部分,代表使用 DSL 時沒有動作的概念。

下一步是定義一些常數,可以用於定義圖表。

為圖表建立常數

系統會將基於 XML 的導覽圖剖析為 Android 建構程序的一部分。系統將為圖表中定義的每個 id 屬性建立數字常數。在執行階段中建構導覽圖時,無法使用這些建構時間產生的靜態 ID,因此導覽 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
        }
    }
}

在這個範例中,結尾的 lambda 使用 fragment() DSL 建構工具函式定義了兩個片段目的地。此函式要求目的地的路徑字串,該字串從常數中取得。此函式也接受選用的 lambda 來進行其他設定 (例如目的地標籤),以及用於引數和深層連結的嵌入式建構工具函式。

管理每個目的地 UI 的 Fragment 類別會做為用角括弧 (<>) 括起來的參數化類型來傳送。這與在使用 XML 定義的片段目的地上設定 android:name 屬性的作用相同。

最後,您可以使用標準 NavController.navigate() 呼叫,從 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 針對三種目的地類型提供內建支援,這三種類型為 FragmentActivityNavGraph 目的地。每個目的地都有專屬的內嵌擴充功能,可用來建構及設定這個目的地。

片段目的地

fragment() DSL 函式可經過參數化處理,轉換為實作的片段類別,並使用專屬路徑字串指派給這個目的地,目的地後方會接 lambda 函式,您可在該函式中提供其他設定,如「使用 Kotlin DSL 圖表進行導覽」一節所述。

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

活動目的地

activity() DSL 函式使用專屬路徑字串來指派給這個目的地,但不會參數化至任何實作活動類別。您可在結尾的 lambda 中設定選用的 activityClass。這種靈活性讓您能夠為應使用隱含意圖 (而非明確的活動類別) 啟動的活動定義活動目的地。與片段目的地一樣,您也可以設定標籤、引數和深層連結。

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

   activityClass = ActivityDestination::class
}

navigation() DSL 函式可用來建構巢狀導覽圖。這個函式使用三個引數:指派給圖表的路徑、圖表的起始目的地路徑,以及用於進一步設定圖表的 lambda。有效的元素包括其他目的地、引數、深層連結,以及目的地的描述性標籤。在使用 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 的 lambda。

在 lambda 中,您可以指定引數資料類型、預設值 (如適用),以及是否可為空值。

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 和從中還原):

@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 導覽資源。 因此,您不能依賴於將 <nav-graph> 元素放入 AndroidManifest.xml 檔案,而是必須手動將意圖篩選器新增至活動。您提供的意圖篩選器應符合應用程式深層連結的基本網址模式、動作和 MIME 類型。

您可以使用 deepLink() DSL 函式,為每個深層連結的目的地提供更具體的 deeplink。此函式接受 NavDeepLink,其中包含代表 URI 模式的 String、代表意圖動作的 String,以及代表 MIME 類型的 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 不相容,因為外掛程式會尋找 XML 資源檔案來產生 DirectionsArguments 類別。

瞭解詳情

如要瞭解如何為 Kotlin DSL 和 Navigation Compose 程式碼提供類型安全,請參閱導覽類型安全頁面。