【Android】Navigationの遷移先にあわせてToolbarをカスタマイズする

# はじめに
みなさん、[Navigation Component](https://developer.android.com/guide/navigation)使ってますか？

Fragment間の画面遷移や値の受け渡しなどを非常に楽にしてくれるNavigationですが、ToolBarと併せて使用した時にいくつか詰まったポイントがありましたので、本記事で紹介したいと思います。

※2020/02/14 FragmentContainerViewについて追記しました

# この記事で紹介すること
- Navigation + Toolbarを扱う時に困りがちなことの解決方法

# この記事で紹介しないこと
- Navigationの基本的な使い方

Navigationの使い方を知りたい場合は[こちら](https://qiita.com/jimmysharp/items/06f3157152fbc4cd5113)の記事が参考になります。

# リポジトリ
こちらに今回のサンプルを置いておきます。
https://github.com/nanaten/Navigation-and-Toolbar-Sample

# 前提
Activityのレイアウトはこんな感じです。
ActivityにToolbarとFragmentを乗っけています。
※`style` は`NoActionBar`にしています。

```xml:activity_main.xml
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.appbar.AppBarLayout>

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/appbar"
        app:navGraph="@navigation/main_nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

# Tips
## 1. 特定のFragmentだけUpアイコンを表示させない
ログイン後のホーム画面など、この画面にはUpアイコン（←）を表示させたくない、という場合があると思います。
そういう時は `AppBarConfigration` にUpアイコンを表示させたくないFragmentのIDを `Set` で渡します。

```kotlin
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(setOf(R.id.firstFragment))
setupActionBarWithNavController(navController, appBarConfiguration)
```

アイコンを表示させたくないFragmentが複数ある場合は、以下のように複数のIDを渡せます。

```kotlin
val appBarConfiguration = AppBarConfiguration(setOf(R.id.firstFragment, R.id.secondFragment))
```


## 2. 特定のFragmentだけToolbarを消したい
特定のFragmentに移動した時だけToolbarを表示させないようにしたい！と思ったことはありませんか？
Fragmentから操作しても良いのですが、なんとなくFragmentからActivityにあるコントロールを操作するのは気持ち悪い…
と思っていたら、`navController.addOnNavigationDestinationListener`という便利なリスナーが存在しました。

```kotlin
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->  }
```

例えば `ThirdFragment` に移動した時だけToolbarを消したい、という場合は以下のように使えます。

```kotlin
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ ->
    toolbar.visibility = if(destination.id == R.id.thirdFragment) View.GONE else View.VISIBLE
}
```

## 3. タイトルを動的に切り替える
例えばリスト選択→詳細表示へ遷移した際に、選択した項目に合わせて動的にタイトルを設定したいという場合、以下の方法が使えます。

1. NavGraphで設定する
2. `addOnDestinationChangedListener` で設定する

### 準備
どちらの方法も準備は同じです。
NavGraphでタイトル用のargumentを設定します。

```xml:NavGraph
    <fragment
        android:id="@+id/fourthFragment"
        ...>
        <argument
            android:name="title"
            app:argType="string" />
    </fragment>
```

SafeArgsでFragmentに引数を渡してあげます。
SafeArgsについては[こちらの記事](https://qiita.com/tktktks10/items/c78fe6f2083611676fbb)が参考になります。

```kotlin
val title = "test title"
val args = FirstFragmentDirections.actionFirstToFourth(title)
findNavController().navigate(args)
```

### 1. NavGraphで設定する
NavGraphで設定する場合は、fragmentのラベルに以下のように引数名を設定します。

```xml:NavGraph
    <fragment
        android:id="@+id/fourthFragment"
        ...
        android:label="{title}">
        <argument
            android:name="title"
            app:argType="string" />
    </fragment>
```

これだけで引数に渡した値がタイトルとして表示されるようになります！

### 2. addOnDestinationChangedListenerで設定する
こちらは先ほどの `addOnDestinationChangedListener` を用いた方法になります。
3番目の引数 `arguments` からFragmentへ渡したargumentが取得できるため、そちらを利用します。

```kotlin
navController.addOnDestinationChangedListener { _, _, arguments ->
    if(arguments?.getString("title") != null) {
        supportActionBar?.title = arguments.getString("title")
    }
}
```

`destination` でIDを判別しても良いですが、上記のようにargumentsの取得結果がnullかどうかで判別すれば、複数箇所で `title` というargumentを使っている場合に対応が簡単になります。

Toolbarの設定をActivityで一括管理したい場合は `addOnDestinationChangedListener` を使うのも一つの手かもしれません。

# おまけ　FragmentContainerViewを使う場合のfindNavControllerのやり方
自分でやってみて詰まったのでメモ代わりに追記しておきます。
`FragmentContainerView` とは、今までFragmentのコンテナとして利用されていた`FrameLayout`の代替Viewとして登場したものです。
詳しい解説は[こちらの記事](https://qiita.com/yoppie_x/items/30b91d63f55b775af3c5)が参考になります。

レイアウトの `fragment` を以下のように置き換えることが可能ですが、**普通にonCreateで `findNavController` すると「NavControllerが見つからない」というエラーが発生します。**

```xml:activity_main.xml
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    ...

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/appbar"
        app:navGraph="@navigation/main_nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

```kotlin:エラーが出る書き方
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val navController = findNavController(R.id.nav_host_fragment)
    ...
}
```

[こちら](https://issuetracker.google.com/issues/142847973)にIssueがありますが、`FragmentContainerView` に対して、onCreateの段階では `fragmentManager` がINITIALIZING状態にあるために起きているそうです（たぶん…）

`FragmentContainerView` を使う場合は、 `navController` の取得方法を以下のように修正する必要があります。

```kotlin
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
```
`FragmentContainerView` から直接ではなく、`FragmentContainerView` にアタッチしたNavHostFragmentから取得しています（たぶん…）

# おわりに
本記事を書くにあたって、以下のリンク先を参考にさせて頂きました。
・[[stsnブログ] メモ Android: Navigation Component + Toolbar(ActionBar)周りのコードを読んで見る](https://satoshun.github.io/2019/11/navigation-toolbar-internal-code/)
・[[Qiita] 特定のFragmentが表示される時にAppBarLayoutとBottomNavigationViewを非表示にする](https://qiita.com/sekitaka_1214/items/0120bbdb9c1feacc3423)
・[[Kenji Abe] Navigationでタイトルを設定する](https://medium.com/@star_zero/navigation%E3%81%A7%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B-9a20807ecc31)