Kotlin DSL を使用してプログラムでグラフを作成する

Navigation コンポーネントが提供する Kotlin ベースのドメイン固有言語(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 の 2 つのデスティネーションがあります。home デスティネーションは、ユーザーがアプリを初めて起動したときに表示され、ユーザーの庭にある植物のリストを示します。ユーザーが植物の 1 つを選択すると、アプリは plant_detail デスティネーションに移動します。

図 1 に、これらのディスティネーションと、アプリが home から plant_detail に移動する際に使用するアクション to_plant_detail と、plant_detail デスティネーションで必要となる引数を示します。

Sunflower アプリの 2 つのデスティネーションと、それらを接続するアクション。
図 1: Sunflower アプリの homeplant_detail の 2 つのデスティネーションと、それらを接続するアクション。

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 を 1 つ以上の引数と関連付けます。ただし、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 ビルダー関数を使用し、後置ラムダで 2 つのフラグメントのデスティネーションを定義しています。この関数には定数から取得するデスティネーションのルート文字列が必要です。デスティネーションのラベルなど、設定を追加する場合はオプションのラムダに加え、引数とディープリンクの埋め込みビルダー関数も受け取ります。

各デスティネーションの UI を管理する Fragment クラスは、パラメータ化された型として山かっこ(<>)で囲まれて渡されます。これは XML を使用して定義されたフラグメント デスティネーションで android:name 属性を設定するのと同じ役割を果たします。

最後に、標準の NavController.nav() 呼び出しを使用して 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 という 3 つのデスティネーション タイプのサポートが組み込まれています。各デスティネーションには、デスティネーションの作成と構成に使用できる独自のインライン拡張関数があります。

Fragment デスティネーション

fragment() DSL 関数は、実装するフラグメント クラスにパラメータ化できます。この関数は、このデスティネーションに割り当てる一意のルート文字列に続いて、ラムダを受け取ります。ここでは Kotlin DSL グラフで移動するに記載のとおり、追加の設定ができます。

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

Activity デスティネーション

activity() DSL 関数は、このデスティネーションに割り当てる一意のルート文字列を受け取りますが、実装したアクティビティ クラスにパラメータ化されません。代わりに、後置ラムダでオプションの activityClass を設定できます。この柔軟性によって、明示的なアクティビティ クラスでは意味を成さない場合に、暗黙的インテントを使用して起動しなければならないアクティビティの Activitiy デスティネーションを定義できます。Fragment デスティネーションと同様に、ラベル、引数、ディープリンクを設定することもできます。

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

   activityClass = ActivityDestination::class
}

navigation() DSL 関数を使用して、ネストされたナビゲーション グラフをビルドできます。この関数は、グラフに割り当てるルート、グラフの開始デスティネーションのルート、グラフの詳細を構成するラムダという、3 つの引数を受け取ります。有効な要素には、他のデスティネーション、引数、ディープリンク、デスティネーションの説明ラベルなどがあります。このラベルは NavigationUI を使用してナビゲーション グラフを UI コンポーネントにバインドする場合に役立ちます。

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

カスタム デスティネーションのサポート

Kotlin DSL を直接サポートしていない新しいデスティネーション タイプを使用している場合、これらのデスティネーションを Kotlin DSL に追加するには、addDestination() を使用します。

// 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
}

デスティネーション引数を指定する

デスティネーションでは、オプションまたは必須の引数を指定できます。アクションはすべてのデスティネーション ビルダータイプの基本クラスである NavDestinationBuilderargument() 関数を使用して指定できます。この関数は 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 Serialization ライブラリまたはその他のライブラリを使用して、カスタム型のリフレクションのないエンコードとデコードが可能になります。

たとえば、検索画面に渡される検索パラメータを表すデータクラスは、Serializable(エンコードとデコードをサポート)と ParcelizeBundle からの保存と復元をサポート)の両方を実装できます。

@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 Serialization を使用して文字列の値を解析します。つまり、デスティネーションに移動するときに形式が一致するように Kotlin Serialization も使用する必要があります。

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 タイプと一致する必要があります。

個別にディープリンクされたデスティネーションごとに、より具体的な deeplinkdeepLink() DSL 関数を使用して指定できます。この関数は URI パターンを表す String、インテント アクションを表す String、mimeType を表す String を含む NavDeepLink を受け取ります。

次に例を示します。

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 プラグインは、Directions クラスと Arguments クラスを生成する XML リソース ファイルを参照するため、Kotlin DSL とは互換性がありません。

詳細

Kotlin DSL と Navigation Compose のコードで型安全性を実現する方法については、Navigation の型安全性のページをご覧ください。