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

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

依存関係

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

Groovy

dependencies {
    def nav_version = "2.5.3"

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

Kotlin

dependencies {
    val nav_version = "2.5.3"

    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 ナビゲーション グラフのホストを作成する

グラフの作成方法にかかわらず、NavHost でグラフをホストする必要があります。Sunflower はフラグメントを使用するため、次の例のように 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>

この例では、グラフは XML リソースとして定義されるのではなくプログラムによって作成されるため、app:navGraph 属性が設定されていないことに注意してください。

グラフの定数を作成する

XML ベースのナビゲーション グラフを使用する場合、Android のビルドプロセスはグラフリソース ファイルを解析し、グラフで定義された id 属性ごとに数値定数を定義します。これらの定数には、生成されたリソースクラス R.id を介してコードからアクセスできます。

たとえば、次の XML グラフ スニペットは、idhome を使用してフラグメント デスティネーションを宣言します。

<navigation ...>
   <fragment android:id="@+id/home" ... />
   ...
</navigation>

ビルドプロセスは、このデスティネーションに関連付けられた定数値 R.id.home を作成します。コードからこのデスティネーションを参照するには、この定数値を使用します。

Kotlin DSL を使用してプログラムでグラフを作成する場合、このような解析と定数の生成は行われません。代わりに、id 値を持つデスティネーション、アクション、引数ごとに独自の定数を定義する必要があります。各 ID は一意でなければならず、設定が変わっても一貫性を維持する必要があります。

定数を系統立てて作成する方法としては、次の例に示すように、Kotlin object のセットをネストにして作成し、定数を静的に定義する方法があります。

object nav_graph {

    const val id = 1 // graph id

    object dest {
        const val home = 2
        const val plant_detail = 3
    }

    object action {
        const val to_plant_detail = 4
    }

    object args {
        const val plant_id = "plantId"
    }
}

この構造の場合、コード内で ID 値にアクセスするには、次の例に示すようにオブジェクトの呼び出しをチェーン接続します。

nav_graph.id                     // graph id
nav_graph.dest.home              // home destination id
nav_graph.action.to_plant_detail // action home -> plant_detail id
nav_graph.args.plant_id          // destination argument name

ID の最初のセットを定義したら、ナビゲーション グラフを作成できます。NavController.createGraph() 拡張関数を使用して NavGraph を作成し、グラフの idstartDestination の ID 値、グラフの構造を定義する後置ラムダを渡します。

アクティビティの onCreate() 関数でグラフを作成できます。createGraph()Navgraph を返します。これは、NavHost に関連付けられた NavControllergraph プロパティに代入できます。

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_garden)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.navController.apply {
            graph = createGraph(nav_graph.id, nav_graph.dest.home) {
                fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
                    label = getString(R.string.home_title)
                    action(nav_graph.action.to_plant_detail) {
                        destinationId = nav_graph.dest.plant_detail
                    }
                }
                fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
                    label = getString(R.string.plant_detail_title)
                    argument(nav_graph.args.plant_id) {
                        type = NavType.StringType
                    }
                }
            }
        }
    }
}

この例では、fragment() DSL ビルダー関数を使用し、後置のラムダ構文で 2 つのフラグメントのデスティネーションを定義しています。この関数にはデスティネーションの ID を必須とし、デスティネーションの label など、設定を追加する場合はオプションのラムダを受け入れます。また、アクション、引数、ディープリンク用の埋め込みビルダー関数も受け入れます。

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

グラフを作成して設定すると、次の例のように NavController.navigate() を使用して home から plant_detail に移動できます。

private fun navigateToPlant(plantId: String) {

    val args = bundleOf(nav_graph.args.plant_id to plantId)

    findNavController().navigate(nav_graph.action.to_plant_detail, args)
}

サポートされるデスティネーション タイプ

Kotlin DSL は FragmentActivityNavGraph デスティネーションをサポートしており、デスティネーションの作成と設定のためのインライン拡張関数がそれぞれに存在します。

Fragment デスティネーション

fragment() DSL 関数は、その実装である Fragment クラスにパラメータ化できます。この関数は、このデスティネーションに割り当てる一意の ID を取ります。追加の設定を提供する場合はラムダも取ります。

fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
   label = getString(R.string.fragment_title)
   // arguments, actions, deepLinks...
}

Activity デスティネーション

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

activity(nav_graph.dest.activity_dest_id) {
    label = getString(R.string.activity_title)
    // arguments, actions, deepLinks...

    activityClass = ActivityDestination::class
}

navigation() DSL 関数を使用して、ネストされたナビゲーション グラフを作成できます。他のデスティネーション タイプと同様に、この DSL 関数は、グラフに割り当てる ID、グラフの開始デスティネーション ID、グラフの設定を追加するためのラムダ、という 3 つの引数を受け取ります。ラムダの有効な要素には、引数、アクション、他のデスティネーション、ディープリンク、ラベルがあります。

navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
   // label, arguments, actions, other destinations, deep links
}

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

デフォルトで直接サポートされていないカスタム デスティネーションを Kotlin DSL に追加するには、次の例に示すように addDestination() を使用します。

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)

また、次のように、単項プラス演算子(+)を使用して、新しく作成されたデスティネーションをグラフに直接追加することもできます。

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}

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

いずれのデスティネーション タイプにも、オプションまたは必須の引数を定義できます。引数を定義するには、すべてのデスティネーションのビルダータイプの基本クラスである NavDestinationBuilderargument() 関数を呼び出します。この関数は引数の名前を String として取り、NavArgument の作成と設定に使用できるラムダも取ります。ラムダ内では、引数のデータ型、デフォルト値(該当する場合)、引数の値を null にできるかどうかを指定できます。

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_name)
        nullable = true  // default false
    }
}

defaultValue を指定する場合、type は省略可能です。この場合、type を指定しなければデータ型は defaultValue から推測されます。defaultValuetype の両方を指定する場合、データ型を一致させる必要があります。引数のデータ型の一覧については、NavType をご覧ください。

アクション

上記に説明したいずれのデスティネーション内でもアクションを定義できます。これには、ルート ナビゲーション グラフ内のグローバル アクションも含まれます。アクションを定義するには、NavDestinationBuilder.action() 関数を使用し、関数の ID を指定します。その他の設定にはラムダを指定します。

次の例では、destinationId、遷移アニメーション、ポップ動作とシングルトップ動作を含むアクションを作成します。

action(nav_graph.action.to_plant_detail) {
    destinationId = nav_graph.dest.plant_detail
    navOptions {
        anim {
            enter = R.anim.nav_default_enter_anim
            exit = R.anim.nav_default_exit_anim
            popEnter = R.anim.nav_default_pop_enter_anim
            popExit = R.anim.nav_default_pop_exit_anim
        }
        popUpTo(nav_graph.dest.start_dest) {
            inclusive = true // default false
        }
        // if popping exclusively, you can specify popUpTo as
        // a property. e.g. popUpTo = nav_graph.dest.start_dest
        launchSingleTop = true // default false
    }
}

ディープリンク

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

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

個別にディープリンクされたデスティネーションごとに deepLink() DSL 関数を使用して、より具体的な URI パターンを指定できます。次の例に示すように、この関数は URI パターンを String で指定します。

deepLink("http://www.example.com/plants/")

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

以下に、パスベースとクエリベースのパラメータを定義して、より複雑な暗黙的ディープリンクを指定する場合について示します。

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

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    deepLink("${baseUri}/{id}")
    deepLink("${baseUri}/{id}?name={plant_name}")
    argument(nav_graph.args.plant_id) {
       type = NavType.IntType
    }
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        nullable = true
    }
}

文字列補間を使用すると、定義を簡略化できます。

ID を作成する

Navigation ライブラリでは、グラフ要素に使用される ID 値は、設定が変更されても一貫性を保つ一意の整数である必要があります。これらの ID を作成する方法の 1 つとしては、グラフの定数を作成するで示すような静的定数として定義する方法があります。XML でリソースとして静的リソース ID を定義することもできます。また、ID を動的に作成することもできます。たとえば、参照するたびに増分するシーケンス カウンタを作成できます。

object nav_graph {
    // Counter for id's. First ID will be 1.
    var id_counter = 1

    val id = id_counter++

    object dest {
       val home = id_counter++
       val plant_detail = id_counter++
    }

    object action {
       val to_plant_detail = id_counter++
    }

    object args {
       const val plant_id = "plantId"
    }
}

制限事項

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