NavigationとSafeArgsを既存プロジェクトに適用する

# はじめに
既存のプロジェクトでは、複数のアクティビティによる画面遷移の構成となっておりましたが、Androidアプリとしてデファクトスタンダードな構成とするため

- Single Activity, Multiple Fragments化
- Navigationによる画面遷移
- SafeArgsによるパラメータの引き渡し

を適用しました。
なので、この記事では上記の適用方法について紹介していきたいと思います。

# プロジェクトの設定
NavigationとSafeArgsのために、
Projectの`app/build.gradle`に以下を設定
```gradle
buildscript {    
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3"
    }
}
```
appの`build.gradle`に以下を設定
```gradle
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'kotlin-parcelize'

dependencies {
    implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
    implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
}
```

# Single Activity, Multiple Fragments化
### ActivityをFragmentに変換する
これまでActivityとして作成したクラスは`AppCompatActivity`の継承から`Fragment`の継承に変更する修正が基本になります。
以下のようにアクションバーにタイトルの設定や戻るボタンを設定している場合は以下のように変換します。
#### 変換前のActivity
```kotlin
class LicenseActivity : AppCompatActivity() {
    @SuppressLint("ResourceType")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_license)
        // タイトルの設定
        title = getString(R.string.license)
        // 戻るボタンの設定
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
    }
}
```
#### 変換後のFragment
```kotlin
class LicenseFragment : Fragment() {
    /** ライセンス画面のビュー */
    private var rootView: View? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Fragmentのメニューを有効にする
        setHasOptionsMenu(true)
    }

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // タイトルの設定
        requireActivity().title = getString(R.string.license)
        // 戻るボタンの設定
        (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
    }
}
```
### Single Activityを用意
`Fragment`全体を管理する`Activity`を以下のように１つ用意します。
```kotlin
class PasswordMemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setTheme(R.style.AppTheme)
        setContentView(R.layout.activity_password_memo)
    }
}
```
`xml`ファイルの中で、後述するNavigationのファイルを設定します。
```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/loginView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.highcom.passwordmemo.PasswordMemoActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/passwordmemo_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"/>
</androidx.constraintlayout.widget.ConstraintLayout>
```

# Navigation Editorで画面遷移を設定
### nav_graphの設定
`res/navigation/nav_graph.xml`を作成して、以下のようにDesignタブでグラフィカルに画面遷移を設定します。
![nav_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545703/9bc8b241-8ef6-3582-d32b-069f955055d6.png)
- `passwordListFragment`→`inputPasswordFragment`
- `inputPasswordFragment`→`passwordListFragment`

の遷移について`xml`ファイルの設定は以下となります。
```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/passwordmemo_nav_graph"
    app:startDestination="@id/loginFragment">

    <fragment
        android:id="@+id/passwordListFragment"
        android:name="com.highcom.passwordmemo.ui.fragment.PasswordListFragment"
        android:label="fragment_password_list"
        tools:layout="@layout/fragment_password_list" >
        <action
            android:id="@+id/action_passwordListFragment_to_inputPasswordFragment"
            app:destination="@id/inputPasswordFragment"
            app:popUpTo="@id/passwordListFragment"
            app:enterAnim="@anim/slide_in_left"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_right"
            app:popExitAnim="@anim/slide_out_right" >
            <argument
                android:name="editData"
                app:argType="com.highcom.passwordmemo.ui.PasswordEditData" />
        </action>
    </fragment>
    <fragment
        android:id="@+id/inputPasswordFragment"
        android:name="com.highcom.passwordmemo.ui.fragment.InputPasswordFragment"
        android:label="InputPasswordFragment"
        tools:layout="@layout/fragment_input_password" >
        <action
            android:id="@+id/action_inputPasswordFragment_to_passwordListFragment"
            app:destination="@id/passwordListFragment"
            app:popUpTo="@id/passwordListFragment"
            app:popUpToInclusive="true"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_right"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_left" />
        <argument
            android:name="editData"
            app:argType="com.highcom.passwordmemo.ui.PasswordEditData" />
    </fragment>
</navigation>
```

### Animationの設定
```nav_graph.xml
            app:enterAnim="@anim/slide_in_left"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_right"
            app:popExitAnim="@anim/slide_out_right" 
```
上記の設定に対するAnimationファイルを設定します。
`res/anim`フォルダ配下に以下のファイルを設定。
```slide_in_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="100%"
        android:toXDelta="0%" />
</set>
```
```slide_out_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="0%"
        android:toXDelta="-100%" />
</set>
```
```slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="-100%"
        android:toXDelta="0%" />
</set>
```
```slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="0%"
        android:toXDelta="100%" />
</set>
```
この設定をすることで、Navigationによる画面遷移時に、左から右にスライドするような画面遷移の挙動になります。

# SafeArgsによるパラメータの引き渡し
### NavigationにSafeArgsを設定
```nav_graph.xml
            <argument
                android:name="editData"
                app:argType="com.highcom.passwordmemo.ui.PasswordEditData" />
```
`action`タグの中に`argument`を設定する事でデータの受け渡しが実現できます。
`app:argType`に設定するのは基本的にはプリミティブ型なのですが、データクラスのような値を渡したい場合には、以下のような`Parcelable`を継承したデータクラスを作成します。
```kotlin
package com.highcom.passwordmemo.ui

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class PasswordEditData(
    val edit: Boolean = false,
    val id: Long = 0,
    val title: String = "",
    val account: String = "",
    val password: String = "",
    val url: String = "",
    val groupId: Long = 1,
    val memo: String = "",
    val inputDate: String = "",
) : Parcelable
```

### Navigationを利用した画面遷移の呼び出し
```kotlin
    val passwordEditData = PasswordEditData()
    findNavController().navigate(PasswordListFragmentDirections.actionPasswordListFragmentToInputPasswordFragment(editData = passwordEditData))
```
上記のように`findNavController().navigate`で必要な画面遷移のアクションを呼び出して、引数にSafeArgsで設定したデータクラスの値を設定して呼び出す事で画面遷移が実現できます。
また、受け取る際には以下のような実装になります。
```kotlin
    /** Navigationで渡された引数 */
    private val args: InputPasswordFragmentArgs by navArgs()
    val passwordEditData = args.editData
```

# さいごに
対応について以上となります。
全体のコードについては、以下のリポジトリで公開しておりますので、参考になれば幸いです。

https://github.com/highcom/PasswordMemoPublic