Navigation Component with Kotlin DSL

この記事は[and factory Advent Calendar 2020](https://qiita.com/advent-calendar/2020/andfactory)の5日目の記事です。
昨日は@ichikawa7ssさんの[【超ミニマム】AWS AppSync + AmplifyでiOSチャットアプリを作る](https://qiita.com/ichikawa7ss/items/3da2842056186e3a5b60)でした。



# はじめに

Navigation ComponentでKotlin DSLを使用できるようになってたので簡単に解説します。
本記事は下記ドキュメントの抜粋なので詳細はこちらをご参照ください。
https://developer.android.com/guide/navigation/navigation-kotlin-dsl?hl=ja

※サンプルコードとしてhttps://github.com/android/sunflower を用いています。

# XMLを使用したNavigation
fragmentやaction, argumentなどナビゲーション関連の情報 (NavGragph) を管理するのに通常は下記のようなXMLリソースを使います。

``` nav_garden.xml
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/view_pager_fragment">

    <fragment
        android:id="@+id/view_pager_fragment"
        android:name="com.google.samples.apps.sunflower.HomeViewPagerFragment"
        tools:layout="@layout/fragment_view_pager">

        <action
                android:id="@+id/action_view_pager_fragment_to_plant_detail_fragment"
                app:destination="@id/plant_detail_fragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/plant_detail_fragment"
        android:name="com.google.samples.apps.sunflower.PlantDetailFragment"
        android:label="@string/plant_details_title"
        tools:layout="@layout/fragment_plant_detail">

        <action
            android:id="@+id/action_plant_detail_fragment_to_gallery_fragment"
            app:destination="@id/gallery_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
        <argument
            android:name="plantId"
            app:argType="string" />
    </fragment>

    <fragment
        android:id="@+id/gallery_fragment"
        android:name="com.google.samples.apps.sunflower.GalleryFragment"
        android:label="@string/plant_details_title"
        tools:layout="@layout/fragment_gallery">
        <argument
            android:name="plantName"
            app:argType="string" />
    </fragment>

</navigation>
```

作成したグラフは、NavHostFragmentの`app:navGraph`として指定することでNavControllerを使った移動が行えるようになります。

``` activity_garden.xml
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <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"
        app:navGraph="@navigation/nav_garden" />

</layout>
```

# Kotlin DSLを使用したNavigation
Kotlin DSLを使うとKotlinのコード内で宣言的にNavigationのグラフを作成できるようになります。
以下の依存関係を追加します。

``` build.gradle
dependencies {
    def nav_version = "2.3.1"

    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
}
```

## ホストの作成
ホストとして用いるFragmentはXMLの場合と同様にNavHostFragmentです。
ただし、NavGraphはKotlinのコードで作成するので削除します。

``` diff
    <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"
-       app:navGraph="@navigation/nav_garden" />
+       app:defaultNavHost="true" />
```

## グラフの定数の作成
XMLでグラフを作成する場合`android:id`を定義すれば`R.id`を介して一意なidにアクセスできましたが、Kotlin DSLでグラフを作成する場合、独自の定数を定義してidを管理する必要があります。
ドキュメントではobjectを用いて定数を定義する方法を挙げています。

``` NavGraph.kt
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"
    }
}
```

## グラフの作成
ActivityのonCreate内でグラフを作成します。
`createGraph()`はNavGraphを返すメソッドで、第三引数のラムダ (`NavGraphBuilder.() -> Unit`)内でDestinationを追加していきます。
下記のコードは`fragment()`DSLでFragment Destinationを作成しています。
`fragment()`メソッドのbuilder内でactionやargumentなどの定義を行うことで画面遷移や引数を追加することができます。

``` GardenActivity.kt
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) { // this: NavGraphBuilder
                // Fragment Destinationを作成
                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
                    }
                }
            }
        }
    }
}
```

## 目的地への移動
`NavController.navigate()`メソッドを用いて移動します。
idはnav_graphで定義しておいたものを使います。

``` GardenPlantingAdapter.kt
private fun navigateToPlant(plantId: String) {

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

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

## Deep Link
`deepLink()`メソッドを用いるだけで作成することができます。
strings.xmlファイル内にディープリンクを定義し、`getString()`メソッドを使って書くことも可能です。

``` GardenActivity.kt
fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_id) {
        type = NavType.StringType
    }
    deepLink("https://www.example.com/plants/{plantId}/")
    // deepLink(getString(R.string.deep_link_plants)) もOK
}
```

ただしここで作成されるのは明示的ディープリンクで、暗黙的ディープリンクを作成したい場合はAndroidManifestにインテントフィルタを追加しなければなりません。
XMLを使ってNavGraphを作成している場合は、AndroidManifestファイルの`<nav-graph>`にそのグラフを指定してあげればよかったのですが、Kotlin DSLではその方法を使えないのですべて自分で定義する必要があります。

``` AndroidManifest.xml
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
    <data
        android:host="www.example.com"
        android:pathPattern="/plants.*"
        android:scheme="https" />
</intent-filter>
```

## その他
- この他にもCustom DestinationやNavOptionsの指定などもKotlin DSLで実装可能
- SafeArgsは互換性なし