［Android］ Navigation で画面遷移するときに Safe Args でパラメータを渡す

# はじめに

`Jetpack`の`Navigation`で `Safe Args` を使って、
パラメータを渡せるサンプルをサクッと実装したいと思います。

# 準備

## `Navigation`をセットアップする

`Navigation` を利用したアプリケーションを作成するための下準備をします。
次の手順で必要なライブラリやクラス、レイアウトを生成していきます。

```groovy:build.gradle(app)
dependencies {
    def nav_version = "2.1.0"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
```

## `Fragment` を作成する

`Navigation` を使った遷移で利用する `Fragment` を作成していきます。
次の手順で `OneFragment`, `TwoFragment`, `ThreeFragment` の 3つを作成します。

1. `Package Name` を右クリックし、`New → Fragment → Fragment(Blank)` を選択する
2. `Fragment Name` に `任意の名称` を入力し `OK` を押す
3. `Fragment` にいらない記述が書かれているので消す
4. `Layout` に `Fragment` に遷移させるための `Button` を追加する
5. `Layout` に `Fragment` を遷移させたときのパラメータを表示される`TextView`を追加する


```Fragment.kt
class OneFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_one, container, false)
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/0d2116e1-7269-7b0a-125a-492af5a98edb.png)


```layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".OneFragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="50dp"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_gravity="bottom"
        android:text="Next" />

</FrameLayout>
```

## `Safe Args`をセットアップする

あと今回は`Safe Args`を利用したアプリケーションを作成するので、
`Safe Args`が利用できるようにセットアップします。
この設定をすると`Safe Args`で利用するクラスが自動的に生成されるようになります。

```groovy:build.gradle(.)
    dependencies {
        def nav_version = "2.1.0"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
```

```groovy:build.gradle(app)
apply plugin: "androidx.navigation.safeargs.kotlin"
```

# 実装

`Safe Args` を次の 2 ステップで実装していきます。

| No | 名称 | 役割 |
| -------- | ------ | ------ |
| 1 | `Navigation`で画面遷移させる | `Nav Host`、`Navigation Graph`、`Nav Controller`を実装して画面遷移できるようにします。 |
| 2 | `Safe Args`でパラメータを渡す | `Safe Args`で遷移先`Fragment`にパラメータを渡せるようにします。  |

## Step1 `Navigation`で画面遷移させる

`Safe Args`でパラメータを引き渡すため基本となる画面遷移を実装していきます。

### Nav Graph の作成する

`nav_graph.xml`を作成し、次の内容の`Navigation Editor`で記述します。
`Nav Graph`の作成の詳しい方法は[こちら](https://qiita.com/kaleidot725/items/2fa54afa90c6c0d51c51)に書いてあるので参考にしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/92ac3ebc-908f-bbcc-0277-5866c13ee36d.png)

```nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<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"
    android:id="@+id/nav_graph"
    app:startDestination="@id/oneFragment">

    <fragment
        android:id="@+id/oneFragment"
        android:name="c.kaleidot725.safeargssample.OneFragment"
        android:label="fragment_one"
        tools:layout="@layout/fragment_one" >
        <action
            android:id="@+id/action_oneFragment_to_twoFragment"
            app:destination="@id/twoFragment" />
    </fragment>
    <fragment
        android:id="@+id/twoFragment"
        android:name="c.kaleidot725.safeargssample.TwoFragment"
        android:label="fragment_two"
        tools:layout="@layout/fragment_two" >
        <action
            android:id="@+id/action_twoFragment_to_threeFragment"
            app:destination="@id/threeFragment" />
    </fragment>
    <fragment
        android:id="@+id/threeFragment"
        android:name="c.kaleidot725.safeargssample.ThreeFragment"
        android:label="fragment_three"
        tools:layout="@layout/fragment_three" />
</navigation>
```

### `Nav Host`を作成する

`Main Activity`に`Fragment`を表示するための`Nav Host`を作成します。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
</FrameLayout>
```

### `Nav Controller` を取得して画面を遷移させる

`OneFragment`, `TwoFragment`, `ThreeFragment`で
`Nav Controller`を取得して、`Button`で画面遷移できるようにします。

```OneFragment.kt
class OneFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_one, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val b = view.findViewById<Button>(R.id.next_button)
        b.setOnClickListener {
            this.findNavController().navigate(R.id.action_oneFragment_to_twoFragment)
        }
    }
}
```

いまのところ、こんな感じで動作します。このアプリケーションの
画面遷移時にパラメータを引き渡せるように実装していきます。

![Dec-08-2019 20-23-31.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/bcbe1227-aa39-89ea-1034-c780c89ed8ea.gif)

## Step2 `Safe Args`でパラメータを渡す

そして`Safe Args`でパラメータを引き渡す処理を実装していきます。

### `Navigation Editor`にて`Argument`を作成する

`Navigation Editor`で`Fragment`に`Argument Default Values`を追加して、
遷移元から遷移先に遷移するときにパラメータを指定できるようにします。
次の手順を繰り返して`OneFragment`、`TwoFragment`、`ThreeFragment`にパラメータを指定できるようにします。

1. 遷移先の`Fragment`を選択し、`Attributes`の`Arguments`の`➕`をクリックする
2. `Name`に`title`、`Type`を`String`を選択し、`ADD`をクリックする
3. `1`で選択した`Fragment`に遷移する矢印を選択し、`titile`の`default value`を設定する。

### `<Fragment名称>Directions` `<Fragment名称>Args` が生成されているか確認する

`Navigation Editor`で`Argument`を作成すると画面遷移時のパラメータ受け渡しに必要となる
`<Fragment名称>Directions`、`<Fragment名称>Args`が自動生成されます。

`<Fragment名称>Directions`は遷移元の`Fragment`でパラメータを指定するため、
`<Fragment名称>Args`は遷移先の`Fragment`でパラメータを取得するのに利用します。

今回だと`OneFragmentDirecitons`と`OneFragmentArgs`など、
作成した3つの`Fragment`に対応するクラスが自動生成されているはずです。

```OneFragmentDirections.kt
class OneFragmentDirections private constructor() {
    private data class ActionOneFragmentToTwoFragment(val title: String) : NavDirections {
        override fun getActionId(): Int = R.id.action_oneFragment_to_twoFragment

        override fun getArguments(): Bundle {
            val result = Bundle()
            result.putString("title", this.title)
            return result
        }
    }

    companion object {
        fun actionOneFragmentToTwoFragment(title: String): NavDirections =
                ActionOneFragmentToTwoFragment(title)
    }
}
```

```OneFragmentArgs.kt
data class OneFragmentArgs(val title: String) : NavArgs {
    fun toBundle(): Bundle {
        val result = Bundle()
        result.putString("title", this.title)
        return result
    }

    companion object {
        @JvmStatic
        fun fromBundle(bundle: Bundle): OneFragmentArgs {
            bundle.setClassLoader(OneFragmentArgs::class.java.classLoader)
            val __title : String?
            if (bundle.containsKey("title")) {
                __title = bundle.getString("title")
                if (__title == null) {
                    throw IllegalArgumentException("Argument \"title\" is marked as non-null but was passed a null value.")
                }
            } else {
                throw IllegalArgumentException("Required argument \"title\" is missing and does not have an android:defaultValue")
            }
            return OneFragmentArgs(__title)
        }
    }
}
```

### `<Fragment名称>Directions`で遷移時にパラメータを指定する。

次のように`<Fragment名称>Directions`を利用して、
画面遷移時に`title`パラメータを指定します。

`actionOneFragmentToTwoFragment`にて`title`が指定できます。
今回は`Two Fragment`という文字列を指定しておきます。

```OneFragment.kt
class OneFragment : Fragment() {

    val args : OneFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_one, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val b = view.findViewById<Button>(R.id.next_button)
        b.setOnClickListener {
            val action = OneFragmentDirections.actionOneFragmentToTwoFragment("Two Fragment")
            this.findNavController().navigate(action)
        }
    }
}
```

### `<Fragment名称>Args`で遷移時にパラメータを取得する。

次のように`<Fragment名称>Args`を利用して、
画面遷移先の`Fragment`にてパラメータを取得します。

`TwoFragmentArgs`にて`title`が取得できるようになっています。
今回は取得した`titile`を`TextView`に表示してみます。

```TwoFragment.kt
class TwoFragment : Fragment() {

    val args : TwoFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_two, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val t = view.findViewById<TextView>(R.id.param_view)
        t.text = args.title
    }
}

```

こんな感じで画面遷移時にパラメータを渡せるようになります。
遷移先の`TextView`に遷移元で指定したパラメータが表示できていますね。

![Dec-09-2019 00-10-19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/55604f8c-f505-8a2f-251b-9f5337a19b66.gif)

# おわりに

上記のサンプルを通して、`Safe Args`がどのような動きをするのか、
またはどのようにすれば画面遷移時にパラメータを渡せるのか理解できました。
その内容を簡潔にまとめると次の3点になると思います。

* `Navigation` で `Safe Args`を画面遷移するときにパラメータを受け渡せる。
* `Safe Args`をプロジェクトで利用するようにし、`Navigation Editor`で遷移時のパラメータを設定すると、自動的にパラメータを受け渡すクラスである`<Fragment名称>Directions`と`<Fragment名称>Args`が生成される。
* 生成された`<Fragment名称>Directions`と`<Fragment名称>Args`を利用して、`Navigation`の画面遷移処理を実装すると安全にパラメータを受け渡すことができる。

`Safe Args`を利用する場合のクラス体型を記載すると次のような感じです。
見てわかる通り`Fragment`ごとに必要なクラスが生成されるのでそれを使えばよいようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/8ff878b0-b993-6602-46cf-b4b1e08eefbe.png)


# 参考文献
- [SafeArgs | Sample](https://github.com/kaleidot725/android-mylab/tree/master/SafeArgsSample)
- [Navigation | Android Developers](https://developer.android.com/jetpack/androidx/releases/navigation)