63
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android Advent Calendar 2019

Day 16

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

Last updated at Posted at 2019-12-16

はじめに

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 上に追加されます。(より正確に言うと、新しいタスクが生成されて Activity が再起動し、 back stack が積み直されます)

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

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

まとめ

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

63
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?