フラグメントと Kotlin DSL

Navigation コンポーネントには、Kotlin の型安全性ビルダーに依存する Kotlin ベースのドメイン固有言語(DSL)が用意されています。この API を使用すると、XML リソース内ではなく、Kotlin コード内でグラフを宣言的に作成できます。これは、アプリのナビゲーションを動的に構築する場合に便利です。たとえば、アプリで外部ウェブサービスからナビゲーション構成をダウンロードしてキャッシュに保存し、その構成を使用して、アクティビティの onCreate() 関数でナビゲーション グラフを動的に作成できます。

依存関係

フラグメントで Kotlin DSL を使用するには、アプリの build.gradle ファイルに次の依存関係を追加します。

Groovy

dependencies {
    def nav_version = "2.10.2"

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

Kotlin

dependencies {
    val nav_version = "2.10.2"

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

グラフを作成する

Sunflower アプリに基づく基本的な例を以下に示します。この例では、home と plant_detail の 2 つのデスティネーションがあります。home デスティネーションは、ユーザーがアプリを初めて起動したときに表示されます。このデスティネーションには、ユーザーの庭の植物のリストが表示されます。ユーザーが植物の 1 つを選択すると、アプリは plant_detail デスティネーションに移動します。

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

Sunflower アプリの 2 つのデスティネーションと、それらを接続するアクション。
図 1: Sunflower アプリの home と plant_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 の代わりにシリアル化可能な型を使用します。各ルートは一意のタイプで表されます。

引数を処理するときに、これらはルートタイプに組み込まれます。これにより、ナビゲーション引数の型安全性を確保できます。

@Serializable data object Home
@Serializable data class Plant(val id: String)

ルートを定義したら、ナビゲーション グラフを作成できます。

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

この例では、fragment() DSL ビルダー関数を使用して 2 つのフラグメントのデスティネーションを定義しています。この関数には 2 つの型引数が必要です。

まず、このデスティネーションの UI を提供する Fragment クラス。これを設定すると、XML を使用して定義されたフラグメント デスティネーションで android:name 属性を設定するのと同じ効果があります。

2 つ目はルートです。これは、Any を拡張するシリアル化可能な型である必要があります。このデスティネーションで使用されるナビゲーション引数とそのタイプが含まれている必要があります。

デスティネーションのラベルなど、設定を追加する場合はオプションのラムダに加え、カスタム引数とディープリンクの埋め込みビルダー関数も受け取ります。

最後に、NavController.navigate() 呼び出しを使用して home から plant_detail に移動します。

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

PlantDetailFragment では、現在の NavBackStackEntry を取得し、それに対して toRoute を呼び出してルート インスタンスを取得することで、ナビゲーション引数を取得できます。

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

PlantDetailFragment が ViewModel を使用している場合は、SavedStateHandle.toRoute を使用してルート インスタンスを取得します。

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

このガイドの残りの部分では、一般的なナビゲーション グラフの要素、デスティネーション、グラフの作成時にそれらを使用する方法について説明します。

デスティネーション

Kotlin DSL には、Fragment、Activity、NavGraph という 3 つのデスティネーション タイプのサポートが組み込まれています。各デスティネーションには、デスティネーションの作成と構成に使用できる独自のインライン拡張関数があります。

Fragment デスティネーション

fragment() DSL 関数は、UI のフラグメント クラスと、このデスティネーションを一意に識別するために使用されるルートタイプでパラメータ化できます。その後に、Kotlin DSL グラフで移動するに記載のとおり、追加の設定を指定できるラムダが続きます。

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

Activity デスティネーション

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

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

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

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = 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
}

デスティネーション引数を提供する

デスティネーション引数は、ルートクラスの一部として定義できます。これらは、Kotlin クラスの場合と同じ方法で定義できます。必須の引数は null 値を許容しない型として定義され、オプションの引数はデフォルト値で定義されます。

ルートとその引数を表す基盤となるメカニズムは、文字列ベースです。文字列を使用してルートをモデル化すると、構成の変更やシステムによって開始されたプロセスの終了の際に、ナビゲーション状態をディスクに保存してディスクから復元できます。このため、各ナビゲーション引数はシリアル化可能である必要があります。つまり、引数値のメモリ内表現を String に変換するメソッドが必要です。

Kotlin シリアル化プラグインは、@Serializable アノテーションがオブジェクトに追加されると、基本型のシリアル化メソッドを自動的に生成します。

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

カスタム型を指定する

カスタム引数タイプの場合は、カスタム NavType クラスを指定する必要があります。これにより、ルートまたはディープリンクからの型の解析方法を完全に制御できます。

たとえば、検索画面を定義するために使用されるルートには、検索パラメータを表すクラスを含めることができます。

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@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 serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

その後、その他の型と同様に Kotlin DSL で使用できます。

fragment<SearchFragment, SearchRoute>(
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
    label = getString(R.string.plant_search_title)
}

目的地に移動するときに、ルートのインスタンスを作成します。

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

このパラメータはデスティネーションのルートから取得できます。

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters

ディープリンク

ディープリンクは XML によるナビゲーション グラフと同様に、どのデスティネーションにも追加できます。Kotlin DSL を使用してディープリンクを作成するプロセスには、デスティネーションのディープリンクを作成するで定義されている手順が適用されます。

ただし、暗黙的ディープリンクを作成する場合は、分析して <deepLink> 要素に含める XML ナビゲーション リソースが存在しないため、AndroidManifest.xml ファイルに <nav-graph> 要素を追加する方法は利用できず、代わりに手動でアクティビティにインテント フィルタを追加する必要があります。指定するインテント フィルタは、アプリのディープリンクのベースパス、アクション、MIME タイプと一致する必要があります。

ディープリンクをデスティネーションに追加するには、デスティネーションのラムダ内で deepLink 関数を呼び出します。ルートをパラメータ化された型として受け取り、ディープリンクに使用される URL のベースパスのパラメータ basePath を受け取ります。

deepLinkBuilder の末尾のラムダを使用して、アクションと MIME タイプを追加することもできます。

次の例では、Home の目的地へのディープリンク URI を作成します。

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

URI の形式

ディープリンク URI 形式は、次のルールを使用して、ルートのフィールドから自動的に生成されます。

  • 必須パラメータはパスパラメータとして追加されます(例: /{id})。
  • デフォルト値を持つパラメータ(オプション パラメータ)は、クエリ パラメータとして追加されます(例: ?name={name})。
  • コレクションはクエリ パラメータとして追加されます(例: ?items={value1}&items={value2})。
  • パラメータの順序は、ルート内のフィールドの順序と一致します。

たとえば、次のルートタイプがあるとします。

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

生成される URI の形式は次のとおりです。

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

追加できるディープリンクの数に制限はありません。deepLink() を呼び出すたびに、そのデスティネーションで保持されているリストに新しいディープリンクが追加されます。

制限事項

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