[Navigation Component] Back Stack の変化まとめ

## はじめに

Navigation Component を使う際、[基本原則](https://developer.android.com/guide/navigation/navigation-principles)に従った遷移については本当に楽で助かるのですが、バックボタンの遷移先を前の画面以外にするなど、少し特殊な遷移を実装しなければいけない場合には NavController 内の back stack がどのように積まれているか知る必要が出てきます。
本記事では Navigation Component の使い方を一通り学んだ方向けの情報として、特定の遷移を行った場合の back stack の変化をひたすら挙げていきます。(バージョン2.2.0-rc02での情報です)

なお、本記事には以下の発表内容が一部含まれます。
https://speakerdeck.com/oboenikui/navigation-componentdexian-nizhi-tuteokitakatutapointo

## Navigation Component の Back Stack について

Navigation Component では、 FragmentManager が持つものとは別に back stack を保持します。
この back stack には、 Fragment や DialogFragment 以外にも NavGraph ( Navigation XML でいう`<navigation>`タグのこと) の情報が積まれることが大きな違いです。
Back stack に "グラフ" を積むことに違和感を感じる方もいらっしゃるかもしれません。これは主に Up ナビゲーションで役に立つものですが、まずはそういうものだとご理解ください。

## 起動時の Back Stack

言葉で説明しても具体的なイメージがつかないと思いますので、まず例として以下のようにシンプルなナビゲーション定義の場合を考えます。

```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"
    android:id="@+id/main_navigation"
    app:startDestination="@id/home_fragment">
    <fragment
        android:id="@+id/home_fragment"
        android:name="com.example.HomeFragment" />
</navigation>
```

このとき、起動時に生成される back stack は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/f2dbeb7d-195f-10e1-7f70-b2d8acdbc472.png)

スタック構造ですので、一番上の `home_fragment` が現在の値と考えてください。一番下には `main_navigation` が積まれていますね。 Back stack の一番下には必ずルートの NavGraph が積まれる、ということを覚えてください。


## 新しい画面への遷移

### 同階層の Fragment / DialogFragment への遷移

同階層の (つまり同じ `<navigation>` タグの直下にある) Fragment や DialogFragment への遷移を行う場合、 back stack にはそのまま 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"
    android:id="@+id/main_navigation"
    app:startDestination="@id/home_fragment">
    <fragment
        android:id="@+id/home_fragment"
        android:name="com.example.HomeFragment" />
    <fragment
        android:id="@+id/detail_fragment"
        android:name="com.example.DetailFragment" />
</navigation>
```

例えば、上記定義において、 `home_fragment` から `detail_fragment` に遷移したときは以下のような back stack が形成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/b21817a6-c29c-9e83-70b3-86e966216e48.png)

### 子 NavGraph への遷移

NavGraph は子の NavGraph を持つことができます (これを nested navigation と呼びます)。 `NavController#navigate` メソッドで、 destination id を指定して遷移する場合、子の NavGraph 内の Fragment などに直接遷移することはできませんが、 NavGraph 自体を遷移先として指定することで、`startDestination`に指定された 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"
    android:id="@+id/main_navigation"
    app:startDestination="@id/home_fragment">
    <fragment
        android:id="@+id/home_fragment"
        android:name="com.example.HomeFragment" />
    <navigation
        android:id="@+id/login_navigation"
        app:startDestination="@id/login_fragment">
        <fragment
            android:id="@+id/login_fragment"
            android:name="com.example.LoginFragment" />
    </navigation>
</navigation>
```

例えば上記定義において、 `home_fragment` から `login_fragment` へは、 destination id に `login_navigation` を指定することで実現できます。
このとき、 back stack には子 NavGraph も積まれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/b60b7956-211f-e32f-acf1-3a2b953a0f81.png)

 (ログイン画面を出すときに back stack をクリアする方法については[後ほど説明します)

### 先祖 NavGraph 上にある Fragment への遷移

子 NavGraph への遷移とは異なり、子階層の Fragment から親階層上の Fragment には、直接 destination id を指定して遷移することができます。このときは、同階層の遷移と同様 Fragment のみが back stack に積まれます。
ログイン画面に遷移した状態から、今度は `login_fragment` から `home_fragment` へ遷移させる場合は、次のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/df43fadb-daa5-403c-1e21-55536e548d6a.png)

以上が基本的な遷移です。

### [補足] Activity

Activity に遷移する場合は NavController の back stack には何も積まれません。 Navigation Component 自体、1つの Activity 内での Fragment の遷移を管理するものですので、別の Activity を起動したところで管理するものは何もない、といったところのようです。

### Deep link

Deep link による遷移には大きく分けて3種類の挙動があります。

#### `FLAG_ACTIVITY_NEW_TASK` 付きの Activity 起動による遷移

外部アプリ、もしくは通知などから起動する際に `FLAG_ACTIVITY_NEW_TASK` フラグをつけてActivityを起動すると、 start destination を考慮した back stack が形成されます。 [Explicit deep link](https://developer.android.com/guide/navigation/navigation-deep-link#explicit) による遷移はこちらです。

例えば、大げさな例ですが以下のようなナビゲーション定義がされていた場合を考えます。

```a_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"
    android:id="@+id/a_navigation"
    app:startDestination="@id/a_fragment">
    <fragment
        android:id="@+id/a_fragment"
        android:name="com.example.AFragment" />
    <navigation
        android:id="@+id/b_navigation"
        app:startDestination="@id/b_fragment">
        <fragment
            android:id="@+id/b_fragment"
            android:name="com.example.BFragment" />
        <navigation
            android:id="@+id/c_navigation"
            app:startDestination="@id/c_fragment">
            <fragment
                android:id="@+id/c_fragment"
                android:name="com.example.CFragment">
                <deepLink app:uri="example://nav_sample/" />
            </fragment>
        </navigation>
    </navigation>
</navigation>
```

このとき、 `FLAG_ACTIVITY_NEW_TASK` をつけて `example://nav_sample/` を起動すると、以下のような back stack が形成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/00871bbc-ed72-f1f6-3f62-9fdcff1be328.png)

#### `FLAG_ACTIVITY_NEW_TASK` をつけない Activity 起動による遷移

`FLAG_ACTIVITY_NEW_TASK` をつけない場合、 遷移先を起動するのに最低限の back stack が形成されます。 [Implicit deep link](https://developer.android.com/guide/navigation/navigation-deep-link#implicit) による遷移はこちらです。

上記の `a_navigation.xml` の定義のとき、`FLAG_ACTIVITY_NEW_TASK` をつけずに `example://nav_sample/` を起動すると、以下のような back stack が形成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/501b029b-c551-6b6f-f218-062c4b199525.png)

#### Destination Id の代わりとしての Deep Link

同一 Activity 内の Fragment 遷移にも deep link を用いることができます (2.1.0からの機能)。
今まで説明してきた [`NavController#navigate` メソッドに destination id を指定して遷移するパターン](https://developer.android.com/reference/androidx/navigation/NavController.html#navigate(int,%20android.os.Bundle,%20androidx.navigation.NavOptions,%20androidx.navigation.Navigator.Extras))の場合、同階層もしくは親階層にある destination id しか指定できません。これに対して、 [deep Link を指定する場合](https://developer.android.com/reference/androidx/navigation/NavController.html#navigate(android.net.Uri,%20androidx.navigation.NavOptions,%20androidx.navigation.Navigator.Extras))はアプリ内のどの Fragment へも遷移が可能です。

この際の back stack は、既存の back stack に積み上がる以外は `FLAG_ACTIVITY_NEW_TASK` をつけない Activity 起動による遷移と同じものになります。

上記の `a_navigation.xml` の定義のとき、 `a_fragment` から `c_fragment` に遷移する場合は以下のように back stack が変化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/8a29cb2f-3db1-4764-015f-121f952d14ae.png)


## Pop up, pop back

[基本原則](https://developer.android.com/guide/navigation/navigation-principles)に従い、バックボタンと Up ナビゲーション ( Toolbar の左上の`←`を押したときの挙動) は異なる挙動をすることがあります。

### バックボタン

バックボタンの挙動はシンプルです。 Back stack を pop し続け、 NavGraph 以外の遷移先が見つかったらそこに戻ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/b816d4ae-3fbd-8343-9ebf-ba0976c6a16b.png)

もしもなければ Activity を終了させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/29f09d18-b78f-41b3-f606-81d64c3311d1.png)


### Up ナビゲーション

対して Up ナビゲーションでは back stack を pop していくことは同じですが、 back stack 上に遷移先が存在しない場合、 start destination を考慮した遷移になります。

例えば以下のような場合、 `a_navigation` の startDestination である `a_fragment` , `b_navigation` の startDestination である `b_fragment` は元々 back stack 上に存在しないですが、 Up ナビゲーション後に生成されて back stack 上に追加されます。（より正確に言うと、新しいタスクが生成されて Activity が再起動し、 back stack が積み直されます）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/ca511600-78a5-c854-b595-f9679aa8c2d0.png)

## [発展編] `popUpTo` と組み合わせた画面遷移

新しい画面へ遷移する際に、指定した id まで pop back させてから遷移させることが可能です。

### 例1. Back stack をクリアして新しい画面に遷移

たとえば、ログインしないと使えないアプリにおいて、ログアウトした場合はログイン画面に飛ばす、ということがあります。この場合ログイン画面でバックボタンを押した際にはアプリを終了する、という仕様になることが多いのではないでしょうか。このとき、以下のように `popUpTo` にルートの NavGraph を指定した Action を定義すると、 back stack をクリアできる、と説明されることがよくあります。

```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"
    android:id="@+id/main_navigation"
    app:startDestination="@id/home_fragment">
    <action
        android:id="@+id/action_login"
        app:destination="@+id/login_navigation"
        app:popUpTo="@id/main_navigation"
        app:popUpToInclusive="true"
        app:launchSingleTop="true"/>
    <fragment
        android:id="@+id/home_fragment"
        android:name="com.example.HomeFragment" />
    <navigation
        android:id="@+id/login_navigation"
        app:startDestination="@id/login_fragment">
        <fragment
            android:id="@+id/login_fragment"
            android:name="com.example.LoginFragment" />
    </navigation>
</navigation>
```

例えば上のような定義で `home_fragment` から `action_login` を使って遷移する場合がそれに該当します。このとき、以下のような back stack の変化が起こります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/37b84ae3-2c4f-85e6-c5cc-3fbcad6f3051.png)

（ちなみに、 `popUpToInclusive` を true にしているため、真ん中で back stack が完全にクリアされていますが、実質的には false でも `main_navigation` が残るだけで大きな違いはありません。）
Back stack をクリアしてから遷移すると、 deep link 遷移と同様 back stack には必要な NavGraph が計算されて積まれます。

### 例2. 特定の画面に遷移後は前に戻れないようにする

入力フォームのような画面で、ある画面に到達したら前に戻れなくする、ということも nested navigation を用いれば実現できます。
例として、以下の仕様を考えます。

* メインの画面とは別に入力フォームを持つアプリで、
* 入力フォーム記入後、完了画面を表示し、
* 完了画面では戻るボタンを押したらメインの画面に戻るようにする

```with_input_form_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"
    android:id="@+id/main_navigation"
    app:startDestination="@id/main_fragment">
    <fragment
        android:id="@+id/main_fragment"
        android:name="com.example.MainFragment" />
    <navigation
        android:id="@+id/form_navigation"
        app:startDestination="@id/form1_fragment">
        <fragment
            android:id="@+id/form1_fragment"
            android:name="com.example.form.Form1Fragment">
            <action
                android:id="@+id/action_form1_to_form2"
                app:destination="@+id/form2_fragment"
                app:launchSingleTop="true"/>
        </fragment>
        <fragment
            android:id="@+id/form2_fragment"
            android:name="com.example.form.Form2Fragment">
            <action
                android:id="@+id/action_form2_to_complete"
                app:destination="@+id/complete_fragment"
                app:popUpTo="@id/form_navigation"
                app:popUpToInclusive="false"
                app:launchSingleTop="true"/>
        </fragment>
        <fragment
            android:id="@+id/complete_fragment"
            android:name="com.example.form.CompleteFragment" />
    </navigation>
</navigation>
```

このときは、 `action_form2_to_complete` のように nested navigation における遷移の back stack をクリアする、ということが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15121/72ed003e-13d4-d316-0322-4a6f4caae10c.png)

## まとめ

Navigation Component でよく使われる遷移がどのような back stack の変化を起こすのか、網羅的に記載しました。このあたりの基本的な挙動を理解しておくと対応が楽になると思います。使用する際の参考になれば幸いです。