NavigationComponentのSafeArgsを使ってみる

#SafeArgsとは
画面間のデータの受け渡しをタイプセーフに行うためのものです。

#対象者
既存のアプリにNavigationを使用しての画面遷移の導入は難しいけどタイプセーフに画面間でデータの受け渡しがしたい！

##環境
MacBookPro Catalina
Android Studio ver.4.1.2
Kotlin ver.1.4.30

#導入

project配下のbuild.gradleのdependenciesに以下を追加します。

```gradle:build.gradle
 def nav_version = "2.3.3"
 classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
```
続いてapp/build.gradleに以下を追加します。

```gradle:app/build.gradle#plugins
 id 'androidx.navigation.safeargs.kotlin'
```

```gradle:app/build.gradle#dependencies
implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
implementation "android.arch.navigation:navigation-ui-ktx:1.0.0"
```

#実際に使ってみる

###リソースの追加
まずはリソースにnavigation用ディレクトリを追加します。
![resource1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130827/9cafbd3c-afd2-9838-d93e-429786e97969.png)

次に作成したディレクトリにNavigationResourceFileを適当な名前をつけて追加します(MainActivityで使用するのでmainにした)。
![resource2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130827/eb0c37bf-c214-975d-5596-acb201c6acdb.png)

##Fragmentにデータを渡してみる
###準備
main.xmlの中身を以下のようにして文字列を渡してみます。
構成はActivity+Fragmentです。

argumentの箇所でnameで変数名とargTypeで型を指定します。


```xml:main.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">
    <fragment
        android:name="com.godslew.navigationsample.ui.main.MainFragment">

        <argument
            android:name="title"
            app:argType="string" />
    </fragment>
</navigation>
```

ビルドをするとMainFragmentArgsというものが自動生成されます。
main.xmlで指定したものを格納できるようになっていますね。

###使用する

まずはActivityでMainFragmentを生成する時に文字列を渡します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                    .replace(R.id.container, MainFragment.newInstance("Game"))
                    .commitNow()
        }
    }
}
```
```xml:main_activity.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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />
```

Activityから渡された文字列をargumentsに入れておきます。
取り出すには 以下のコードを書けばOKです。
`private val args: MainFragmentArgs by navArgs()` 

```kotlin:ui/main/MainFragment.kt
class MainFragment : Fragment() {

    companion object {
        fun newInstance(title: String) = MainFragment().apply {
            arguments = MainFragmentArgs(title).toBundle()
        }
    }

    private val args: MainFragmentArgs by navArgs()

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<TextView>(R.id.message).text = args.title
    }
}
```
```xml:main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainFragment">

    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

渡した文字列が画面に反映されました。
![result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130827/88b52834-981e-bcc2-be54-a4844a9378c3.png)



#独自クラスを使用する
実際は単純なデータじゃないものを渡したい場合が多いと思うので紹介します。
selaed classを渡してみたいと思います。
他にも渡せるデータの種類はあります。[^1]

##準備
Parcelableを使いたいのでapp/build.gradleに以下を追加します。

```gradle:app/build.gradle#plugins
 id 'kotlin-parcelize'
```

渡すためのsealed classを作ります。

```kotlin:ui/main/Title.kt

sealed class Title : Parcelable {
    abstract val name: String

    @Parcelize
    data class Game(
        override val name: String = "Game"
    ) : Title()

    @Parcelize
    data class Tv(
        override val name: String = "TV Show"
    ) : Title()

    @Parcelize
    data class Movie(
        override val name: String = "Movie"
    ) : Title()
}

```


##渡してみる
main.xmlを以下のように書き換えます。
argTypeに先ほど作成したsealed classを指定します。

```xml:main.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">
    <fragment
        android:name="com.godslew.navigationsample.ui.main.MainFragment">

        <argument
            android:name="title"
            app:argType="com.godslew.navigationsample.ui.main.Title" />
    </fragment>
</navigation>
```

MainAcivityとMainFragmentを以下のように書き換えます

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                    .replace(R.id.container, MainFragment.newInstance(Title.Game()))
                    .commitNow()
        }
    }
}
```

```kotlin:ui/main/MainFragment.kt
class MainFragment : Fragment() {

    companion object {
        fun newInstance(title: Title) = MainFragment().apply {
            arguments = MainFragmentArgs(title).toBundle()
        }
    }

    private val args: MainFragmentArgs by navArgs()

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<TextView>(R.id.message).text = args.title.name
    }
}
```

先ほどと同じ画面が表示されればOKです。

#まとめ
この記事ではSafeArgsを使ってタイプセーフに画面間でデータを受け渡すための方法を解説しました。
新規でアプリを作る場合にはNavigationComponent全体を導入することは容易だとは思いますが既存の大きなアプリでは導入コストが大きいと思うのでまずはSafeArgsを導入して恩恵を受けてみるのはありなのかなと思います。

#サンプル
[NavigationSample](https://github.com/godslew/NavigationSample)


[^1]: SafeArgs https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args