1. oboenikui

    No comment

    oboenikui
Changes in body
Source | HTML | Preview
@@ -1,277 +1,277 @@
## はじめに
-Navigation Component を使う際、[基本原則](https://developer.android.com/guide/navigation/navigation-principles)に従った遷移については本当に楽で助かるのですが、バックボタンの遷移先を変えるなど、少し特殊な遷移を実装しなければいけない場合には NavController 内の 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 に "グラフ" を積むことに違和感を感じる方もいらっしゃるかもしれません。本記事ではここについて詳しくは触れませんので、気になった方は上記スライドをご覧ください。
## 起動時の 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 が形成されます。 (ログイン画面を出すときに 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 を持つことができます。 `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)
### 先祖 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 上に追加されます。(より正確に言うと、新しい Task が生成されて 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 させてから遷移させることが可能です。なお、公式での呼称は `popUpTo` などになっていますが、挙動的には pop back (バックボタンを押したときの挙動) と同等になってることに注意が必要です。
### 例1. Back stack をクリアして新しい画面に遷移
たとえば、未ログインの場合のみログイン画面に飛ばす場合に、 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 遷移と同様に Fragment の下に必要な NavGraph が計算されて back stack に積まれます。
### 例2. 特定の画面に遷移後は前に戻れないようにする
入力フォームのような画面で、ある画面に到達したら前に戻れなくする、ということも nested navigation を用いれば実現できます。
例えばメインの画面とは別に、入力フォームを持つアプリで、2段階の入力フォーム記入後、完了画面を表示した後は戻るボタンを押したらメインの画面に戻るようにする、という仕様を考えましょう。
```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 の変化を起こすのか、網羅的に記載しました。使う際の参考になれば幸いです。