1. oboenikui

    No comment

    oboenikui
Changes in body
Source | HTML | Preview

はじめに

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

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

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

スタック構造ですので、一番上の 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

子 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

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

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

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

image.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 による遷移はこちらです。

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

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

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 が形成されます。

image.png

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 が変化します。

image.png

Pop up, pop back

基本原則に従い、バックボタンと Up ナビゲーション ( Toolbar の左上のを押したときの挙動) は異なる挙動をすることがあります。

バックボタン

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

image.png

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

image.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

[発展編] 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

ちなみに、 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

まとめ

Navigation Component でよく使われる遷移がどのような back stack の変化を起こすのか、網羅的に記載しました。使う際の参考になれば幸いです。