はじめに
Navigation Component を使う際、基本原則に従った遷移については本当に楽で助かるのですが、バックボタンの遷移先を前の画面以外にするなど、少し特殊な遷移を実装しなければいけない場合には 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
言葉で説明しても具体的なイメージがつかないと思いますので、まず例として以下のようにシンプルなナビゲーション定義の場合を考えます。
<?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 は以下のようになります。
スタック構造ですので、一番上の home_fragment
が現在の値と考えてください。一番下には main_navigation
が積まれていますね。 Back stack の一番下には必ずルートの NavGraph が積まれる、ということを覚えてください。
新しい画面への遷移
同階層の Fragment / DialogFragment への遷移
同階層の (つまり同じ <navigation>
タグの直下にある) Fragment や DialogFragment への遷移を行う場合、 back stack にはそのまま Fragment が積まれます。
<?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 が形成されます。
子 NavGraph への遷移
NavGraph は子の NavGraph を持つことができます (これを nested navigation と呼びます)。 NavController#navigate
メソッドで、 destination id を指定して遷移する場合、子の NavGraph 内の Fragment などに直接遷移することはできませんが、 NavGraph 自体を遷移先として指定することで、startDestination
に指定された Fragment などに遷移することができます。
<?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 も積まれます。
(ログイン画面を出すときに back stack をクリアする方法については[後ほど説明します)
先祖 NavGraph 上にある Fragment への遷移
子 NavGraph への遷移とは異なり、子階層の Fragment から親階層上の Fragment には、直接 destination id を指定して遷移することができます。このときは、同階層の遷移と同様 Fragment のみが back stack に積まれます。
ログイン画面に遷移した状態から、今度は login_fragment
から home_fragment
へ遷移させる場合は、次のようになります。
以上が基本的な遷移です。
[補足] 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 による遷移はこちらです。
例えば、大げさな例ですが以下のようなナビゲーション定義がされていた場合を考えます。
<?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 が形成されます。
FLAG_ACTIVITY_NEW_TASK
をつけない Activity 起動による遷移
FLAG_ACTIVITY_NEW_TASK
をつけない場合、 遷移先を起動するのに最低限の back stack が形成されます。 Implicit deep link による遷移はこちらです。
上記の a_navigation.xml
の定義のとき、FLAG_ACTIVITY_NEW_TASK
をつけずに example://nav_sample/
を起動すると、以下のような back stack が形成されます。
Destination Id の代わりとしての Deep Link
同一 Activity 内の Fragment 遷移にも deep link を用いることができます (2.1.0からの機能)。
今まで説明してきた NavController#navigate
メソッドに destination id を指定して遷移するパターンの場合、同階層もしくは親階層にある destination id しか指定できません。これに対して、 deep Link を指定する場合はアプリ内のどの Fragment へも遷移が可能です。
この際の back stack は、既存の back stack に積み上がる以外は FLAG_ACTIVITY_NEW_TASK
をつけない Activity 起動による遷移と同じものになります。
上記の a_navigation.xml
の定義のとき、 a_fragment
から c_fragment
に遷移する場合は以下のように back stack が変化します。
Pop up, pop back
基本原則に従い、バックボタンと Up ナビゲーション ( Toolbar の左上の←
を押したときの挙動) は異なる挙動をすることがあります。
バックボタン
バックボタンの挙動はシンプルです。 Back stack を pop し続け、 NavGraph 以外の遷移先が見つかったらそこに戻ります。
もしもなければ Activity を終了させます。
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 が積み直されます)
[発展編] popUpTo
と組み合わせた画面遷移
新しい画面へ遷移する際に、指定した id まで pop back させてから遷移させることが可能です。
例1. Back stack をクリアして新しい画面に遷移
たとえば、ログインしないと使えないアプリにおいて、ログアウトした場合はログイン画面に飛ばす、ということがあります。この場合ログイン画面でバックボタンを押した際にはアプリを終了する、という仕様になることが多いのではないでしょうか。このとき、以下のように popUpTo
にルートの NavGraph を指定した Action を定義すると、 back stack をクリアできる、と説明されることがよくあります。
<?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 の変化が起こります。
(ちなみに、 popUpToInclusive
を true にしているため、真ん中で back stack が完全にクリアされていますが、実質的には false でも main_navigation
が残るだけで大きな違いはありません。)
Back stack をクリアしてから遷移すると、 deep link 遷移と同様 back stack には必要な NavGraph が計算されて積まれます。
例2. 特定の画面に遷移後は前に戻れないようにする
入力フォームのような画面で、ある画面に到達したら前に戻れなくする、ということも nested navigation を用いれば実現できます。
例として、以下の仕様を考えます。
- メインの画面とは別に入力フォームを持つアプリで、
- 入力フォーム記入後、完了画面を表示し、
- 完了画面では戻るボタンを押したらメインの画面に戻るようにする
<?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 をクリアする、ということが可能です。
まとめ
Navigation Component でよく使われる遷移がどのような back stack の変化を起こすのか、網羅的に記載しました。このあたりの基本的な挙動を理解しておくと対応が楽になると思います。使用する際の参考になれば幸いです。