Android BottomNavigationView と Navigation併用時の状態保持

# はじめに
AndroidでBottomNavigationViewとNavigationを併用する場合、デフォルトでタブ切り替え時にFragmentが再生成されます。
よって、再生成されないようにするためにはFragmentの状態保持を実装する必要があります。

この実装方法について調べた際、最新バージョンのNavigationを使った実装方法にたどり着けなく、苦労したのでまとめます。

言語はKotlinでバージョンは1.3.31です。

# 導入
```gradle:app/build.gradle
// ...

dependencies {

    // ...

    // Navigation
    def arch_navigation_version = '2.2.0-alpha01'
    implementation "androidx.navigation:navigation-fragment:$arch_navigation_version"
    implementation "androidx.navigation:navigation-fragment-ktx:$arch_navigation_version"
    implementation "androidx.navigation:navigation-ui:$arch_navigation_version"
    implementation "androidx.navigation:navigation-ui-ktx:$arch_navigation_version"

    // Material
    implementation 'com.google.android.material:material:1.0.0'
}
```

# FragmentNavigatorクラスのnavigate()を上書き
Navigationライブラリに実装されている`FragmentNavigator`クラスの`navigate()`でタブ切り替え時にFragmentが再生成されるようになっています。
よって、`FragmentNavigator`クラスを継承した`CustomNavigator`クラスを作成し、状態保持されるように`navigate()`を上書きます。

アニメーション周りの実装とBackStack周りの実装を排除し、状態保持したタブ切り替えのみを行う`navigate()`が以下です。

```kotlin:CustomNavigator.kt
@Navigator.Name("custom_fragment")
class CustomNavigator(
    private val context: Context,
    private val manager: FragmentManager,
    private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {

    @Suppress("DEPRECATION")
    override fun navigate(
        destination: Destination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ): NavDestination? {
        if (manager.isStateSaved) {
            return null
        }

        var className = destination.className
        if (className[0] == '.') {
            className = context.packageName + className
        }

        val tag = destination.id.toString()
        val transaction = manager.beginTransaction()

        val currentFragment = manager.primaryNavigationFragment
        if (currentFragment != null) {
            transaction.hide(currentFragment)
        }

        var fragment = manager.findFragmentByTag(tag)
        if (fragment == null) {
            fragment = instantiateFragment(context, manager, className, args)
            transaction.add(containerId, fragment, tag)
        }
        fragment.arguments = args

        transaction.show(fragment)
        transaction.setPrimaryNavigationFragment(fragment)
        transaction.commit()

        return destination
    }
}
```
# NavHostFragmentクラスのcreateFragmentNavigator()を上書き
次に、Navigationライブラリに実装されている`NavHostFragment`クラスの`createFragmentNavigator()`を上書きます。`FragmentNavigator`クラスのインスタンスが戻り値になっているためです。

`NavHostFragment`を継承した`CustomNavHostFragment`を作成し、上記の`CustomNavigator`クラスのインスタンスが戻り値になるように`createFragmentNavigator()`を上書きます。

```kotlin:CustomNavHostFragment.kt
class CustomNavHostFragment : NavHostFragment() {
    override fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> {
        return CustomNavigator(requireContext(), childFragmentManager, id)
    }
}
```

# navigationレイアウトの作成
`CustomNavigator`クラスにつけた`@Navigator.Name`アノテーションの引数に指定している`custom_fragment`を利用します。

```xml:navigation.kt
<?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"
    android:id="@+id/bottom_navigation"
    app:startDestination="@+id/tab1">

    <custom_fragment
        android:id="@+id/tab1"
        android:name="hoge.HogeFragment"
        android:label="tab1"
        android:tag="tab1" />

    <custom_fragment
        android:id="@+id/tab2"
        android:name="hoge.HogeHogeFragment"
        android:label="tab2"
        android:tag="tab2" />

</navigation>
```

# fragmentとBottomNavigationViewをレイアウトに作成
BottomNavigationViewとFragmentを表示したいレイアウトに`BottomNavigationView`と`fragment`を記載します。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="hoge.MainActivity">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
        </androidx.appcompat.widget.Toolbar>

        <fragment
            android:id="@+id/bottom_navigation_view_fragment"
            android:name="hoge.navigation.CustomNavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view"
            app:layout_constraintTop_toBottomOf="@+id/toolbar"
            app:navGraph="@navigation/navigation" />

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottom_navigation_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?android:attr/windowBackground"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/bottom_navigation_menu" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
以下が注意点です

* fragment
    * `android:name`に`CustomNavHostFragment`を指定
    * `app:navGraph`に上記で記載したnavigationレイアウトを指定
* BottomNavigationView
    * `app:menu`に指定するmenuレイアウトの`item`の`android:id`を`custom_fragment`の`android:id`と揃えること