Navigation導入するなら必ず役に立つ5つのTips

最近、FragmentTransactionからNavigationへ移行しました。

今回の記事では、基本的なNavigationの実装方法などは他にも記事がたくさんあるのでそこは省いています。

基本的な実装というよりは、ここでは設計時に考慮すべきだった事や実装時に気をつけないとクラッシュが起きる所の話などを中心にしています。

Navigationに移行作業中の方や、これからNavigationの導入を検討している方など、誰かの参考になれば嬉しいです。:pray:
# 1. Action と Global Action
**デスティネーション間の経路を視覚的に表示する Action についてのTipsです。**
まず基本的な話ですが、Navigationでは、xmlで書かれた**ナビゲーショングラフ**を使用してアプリのナビゲーション（画面遷移）を管理します。

例えば、Fragmentが３つだけあるアプリのナビゲーショングラフはこんな感じ。

```main_navigation.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/main_navigation"
    app:startDestination="@id/first_fragment">

    <fragment
        android:id="@+id/first_fragment"
        android:name="com.example.sample.FirstFragment"
        tools:layout="@layout/fragment_first">
        <action
            android:id="@+id/action_first_fragment_to_second_fragment"
            app:destination="@id/second_fragment" />
    </fragment>

    <fragment
        android:id="@+id/second_fragment"
        android:name="com.example.sample.SecondFragment"
        tools:layout="@layout/fragment_second">
        <action
            android:id="@+id/action_second_fragment_to_third_fragment"
            app:destination="@id/third_fragment" />
    </fragment>

    <fragment
        android:id="@+id/third_fragment"
        android:name="com.example.sample.ThirdFragment"
        tools:layout="@layout/fragment_third">
    </fragment>

</navigation>
```
この例だと、First → Second → Third というシンプルな流れで、この各経路がActionで定義されています。経路が簡潔でわかりやすいですね。
![スクリーンショット 2020-09-14 12.48.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266675/1830f624-969c-8fc2-59cb-8b537224600e.png)
次に、画面を2つ増やして、さらにどの画面からも最初の画面（FirstFragment）に遷移する経路を作るとなったとき、単純にactionを追加するとこんなグラフになります。
![スクリーンショット 2020-09-14 13.04.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266675/4d3efb97-7eff-7e0d-81c0-9e0934308061.png)
ちょっと線が絡み合って見にくくなりましたね。 

まだFragmentが5個だけで経路も多くないですが、今後Fragmentの数が20, 40, 60,,,と増えると、どことどこが繋がっているのか追いにくくなってしまうのが想像して頂けるかと思います。:thinking:

**で！こんなときは、[Global Action](https://developer.android.com/guide/navigation/navigation-global-action?hl=ja)を使ってやればいいです。:point_up:**
設定は、こんな風にナビゲーショングラフ上のGUIから簡単にできます。
![スクリーンショット 2020-09-14 13.16.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266675/b106b951-82ce-c00e-2fd7-fa926585a332.png)
FirestFragmentの左側に付いている、→ がGlobalActionです。
線が減ってすっきりしましたー！:sparkles:
![スクリーンショット 2020-09-14 13.19.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266675/ff08ca5a-a767-6045-1100-0810cd84020e.png)
このように複数の画面から遷移される画面（たとえばアプリのホーム画面とか）への経路は、```Global Action```で定義するとグラフの線が絡み合わずにすっきり管理することができます。xmlの定義も少なく済むし。:relieved:

ただちょっと注意点が、この```Global Action```が便利すぎるところ。

便利で何が悪いねんって言われそうですが、

極端な例としてあげると、
全経路を```Global Action```で定義したグラフがこれです:point_down:
（もはやグラフになってない）
![スクリーンショット 2020-09-14 13.28.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266675/50ecd402-3bfb-0c5e-d788-91e84866beff.png)
まあこんなことするケースは中々ないとは思いますが、全部Global Actonで定義しても動作的には問題はないって事を言いたかった。ただし、Fragment間に線が表示されないので視覚的に経路が追えなくなりますね。

私は、ラクしたくて気づいたらGlobalActionを結構使っていたので、実装途中で「これでいいのか？」って少し迷ったりしました。:thinking:

アプリの規模やユースケースによるので正解はないと思いますが、```Global Action``` と```通常のAction```をどう使い分けるかは、ナビゲーショングラフの設計時に考慮しましょう。

# 2. Navigationに add Fragmentはない!!
**Navigationには、add Fragmentが存在しないので、```FragmentTransaction``` で```add```を使用している方は注意が必要ですよっていうお話です。**

Navigationでは、replaceで遷移をすることが前提なんですって。:point_up:

そのため、addを使っていた所はreplaceされても問題ないようにFragment側の実装を修正しました。

ただしどうしても```add```の挙動にしたい場合は以下のような対応が選択肢としてあります。参考までに。

- データを```ViewModel```に保存して```onViewCreated（）```でチェックする
- ```<Dialog>```を使用して全画面DialogFragmentsを使用する

# 3. safeArgsで意外に渡せなかった引数の型
**safeArgsに関するTipsです。**
Navigationの便利機能の1つであり、安全に引数の引き渡しが可能になるsafeAegs。このsafeArgsで、意外に引き渡す事ができない型がいくつかありました。

そのうちの1つで個人的に意外だったのが```List```。

ちなみに、```double```, ```short```, ```byte```, ```char```もビルド時にエラーになって使えない。

```List```のように、そのままだと渡せない場合は、`@Parcelize` と組み合わせて data classを作成してそれを引き渡すか、```bundle```で渡す方法がある。

サポートされている引数の型は、[デスティネーション間でデータを渡す](https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja)にまとまっているので、new Instanceで引き渡していた所をSafeArgsにしたいと考えている場合はご一読しとくのがおすすめ。:eyes:

# 4.java.lang.RuntimeException: Unable to start activity 
**各FragmentでNavControllerを使用するときに気をつけたいところです。**
結論から言うと、Fragment の```onCreate()```で```NavController```を取得する実装をしているのがよくなかった。
これがよくない例。

``` FirstFragment.kt
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        navController = findNavController()
}
```
このままだと通常起動時は問題ないですが、Activityが再生成されたときに以下のクラッシュが発生します。

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.sample/com.example.sample.MainActivity}: android.view.InflateException: Binary XML file line #9 in com.example.sample:layout/activity_main: Binary XML file line #9 in com.example.sample:layout/activity_main: Error inflating class fragment
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
```
ログは、```まだ親のActivityが生成されていないのにFragment側でNavControllerを取得しようとするからクラッシュが発生してしまってるよ```と言ってます。
対処としては、Kotlinの場合は、lazyで遅延初期化するよう実装すれば解決します。

``` FirstFragment.kt
private val navController by lazy { findNavController() }
```

Javaの場合、便利なlazyはないので、DaggerやDIする方法がある。
うちではKotlin化を進めているのでこの機会にまだJavaで書かれていたFragmentは全てKotlin化しました。:hatching_chick:

# 5. java.lang.IllegalArgumentException: navigation destination XXX is unknown to this NavController
**各Fragmentでナビゲーションの処理を書くときに気をつけたいところです。**
私の場合は、リリース後にクラッシュが発生して少し困ったので、今後Navigationに移行する人には同じ轍を踏んでほしくない。

そのクラッシュの例がこれ:point_down:
（FirstFragmentでnextボタンを連打してSecondFragmentに遷移しようとしたときのクラッシュ）

```
java.lang.IllegalArgumentException: navigation destination com.example.sample:id/action_first_fragment_to_second_fragment is unknown to this NavController
        at androidx.navigation.NavController.navigate(NavController.java:789)
        at androidx.navigation.NavController.navigate(NavController.java:730)
        at androidx.navigation.NavController.navigate(NavController.java:716)
        at androidx.navigation.NavController.navigate(NavController.java:704)
        at com.example.sample.FirstFragment$onViewCreated$1.onClick(FirstFragment.kt:27)
```
ログは、```現在のNavControllerは、action_first_fragment_to_second_fragmentなんて知らないですよ。```と言っていますね。

**発生原因として考えられるもの**

- 連打
- 複数のナビゲーションの同時押し
- etc.

つまり、FirstFragmentからSecondFragmentへの遷移で発生したクラッシュの一連の流れはこんな感じ。

```
1. FirstFragmentからSecondFragmentへ遷移する経路を高速連打
2. SecondFragmentに遷移しているのに、action_first_fragment_to_second_fragmentを実行しようとする
3. SecondFragmentのNavControllerはそんなDestinationは知らないのでエラーを出す
```

この対処として役に立つのが**currentDestinationチェック**です。:point_up:

```FirstFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    next_button?.setOnClickListener {
        if (navController.currentDestination?.id == R.id.first_fragment) {
            navController.navigate(R.id.action_first_fragment_to_second_fragment)
        }
    }
}
```
今のところ手元で再現できているのが、アニメーション付きで遷移する経路での連打や同時押しですが、アニメーションなしの経路でも同様のクラッシュは発生するようです。

そのため、このcurrentDestinationをチェックする処理はナビゲーションの前に必ず入れた方がよさそうな所感が今のところありますが。もっと良い方法などがあればぜひ教えてください。:bow_tone2:

# おわり
最後まで読んでいただきありがとうございました。