73
73

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 3 years have passed since last update.

AndroidX Navigation による画面遷移の実装まとめ

Last updated at Posted at 2019-10-14

使う際に見つけやすいようにまとめておきます。
基本的に Kotlin のことしか書いていないので、 Java の場合はここを参考にしてください。

導入

Navigation

  • build.gradle (モジュール)
dependencies {
    def nav_version = "【任意のバージョン】"

    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

Safe Args

  • build.gradle (プロジェクト)
dependencies {
    def nav_version = "【任意のバージョン】"
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
  • build.gradle (モジュール)
apply plugin: "androidx.navigation.safeargs.kotlin"

Navigation Component の構成要素

要素 説明
Destination 各画面に相当する Fragment
Action 画面遷移を実現する Destination 同士の繋がり
NavHost 画面のコンテナ。この中で画面が差し替えられて画面遷移が成立
NavController NavHost の画面遷移をコントロール

実装

手順

  • 1つの Activity/Fragment は 1つの Navigation graph しか使えない。
  • Activity を Destination に設定するのは可能。その後の遷移はその Activity の Navigation graph に定義する
    • 単一の Navigation graph に複数の Activity を含めた遷移は定義できない
  1. Navigation graph (XML) を作成
    1. res でコンテキストメニューを開く
    2. "New" > "Android Resource File"
    3. "Resource Type" に Navigation を選択、ファイル名を入力して「OK」
  2. Activity/Fragment に NavHost を配置
    • <fragment>タグで配置
    • NavHostFragment#create() で生成した NavHostFragment を FragmentTransaction で配置
  3. Navigation graph に Destination と Action を定義
  4. NavController#navigate() で遷移を実行
    • Action の ID を指定して実行
    • Fragment の ID を指定して実行
    • Safe Args で Direction から生成した Action オブジェクトを指定して実行

NavHost の配置

大抵の場合、NavHostFragment を使えば大丈夫だと思う。

<fragment> タグで配置

<fragment
    android:id="@+id/(1)"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="(2)"
    app:navGraph="@navigation/(3)" />
  1. <fragment> に付与するID。たとえ使わない場合でも、 IDを指定しないとその画面に遷移した途端に『java.lang.IllegalArgumentException: No view found for id...』でクラッシュする
  2. バックキーとバックスタックの制御を NavHost に任せるか。次項を参照
  3. res/navigation にある Navigation graph

FragmentContainerView で配置してコードで取り出す

<fragment> で配置すると Android Studio のバージョンや設定次第では『Replace the tag with FragmentContainerView.』と警告される場合がある。その場合はこの方法で対処してもよい。

<!-- (1) android:name ではなく class 要素で NavHostFragment を指定 -->
<androidx.fragment.app.FragmentContainerView
    class="androidx.navigation.fragment.NavHostFragment"
    android:id="@+id/navHost"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="(2)"
    app:navGraph="@navigation/(3)" />
// (4)
val navController: NavController =
    (supportFragmentManager.findFragmentById(R.id.navHost as NavHostFragment).navController
  1. 配置する Fragment のクラス名は class 要素 にセット
  2. 前項と同じ。バックキーとバックスタックの制御を NavHost に任せるか
  3. 前項と同じ。res/navigation にある Navigation graph
  4. FragmentContainerView に配置した NavHostFragment を取り出して、そこから navController を取得する

app:defaultNavHost の true or false

app:defaultNavHost の値による挙動の違いは以下の通り。

app:defaultNavHost の値 バックスタック バックキー押下時
true 遷移すると積まれる バックスタックを pop 。無くなったら Activity/Fragment が終了
false 遷移すると積まれる バックスタックに積まれた数に関わらず、 Activity/Fragment が即終了
  • アプリのスタートとなる画面が明確に決まっており、バックキーの操作は以下の通りでよいなら true にするのが適当

    • スタートの画面に戻るまでは前の画面に戻る(バックスタックを pop する)
    • スタートの画面にいるならアプリが終了する
  • アプリのUIにタブがある場合など、アプリのスタートは決まっているが、スタートの画面以外でもアプリが終わる可能性があるなら false にして自分で制御した方がよい

    • false でもバックスタックは積まれるので、 NavController#popBackStack() での判断はできる(3タブの画面でやるなら↓のような感じ)
    override fun onBackPressed() {
        findNavController(R.id.container).run {
            when (currentDestination!!.id) {
                // タブごとの最初の画面
                R.id.tabFragment1, R.id.tabFragment2, R.id.tabFragment3 -> finish()
                // それ以外の画面
                else -> popBackStack()
            }
        }
    }
    

コードで生成して配置

NavHostFragment#create() でインスタンスを生成して FragmentTransaction で配置。

supportFragmentManager
    .beginTransaction()
    .add(R.id.view_id, NavHostFragment.create(R.navigation.nav_graph_name), null)
    .commit()

遷移の実行

Navigation Editor で定義。

NavController の取得

  • Fragment#findNavController()
  • Activity#findNavController(viewId: Int)
    • NavHostFragment を配置した <fragment> の ID を引数に指定
  • View#findNavController()
    • NavHostFragment を配置した View の ID を引数に指定
    • コードで生成した NavHostFragment を add/replace した ViewGroup を findViewById() で取得して、それをレシーバーにするなど

画面 (Fragment) への遷移

  1. Destinations ペインの HOST が NavHostFragment を配置した Activity/Fragment であることを確認
  2. New Destinationをクリックして、任意の Fragment を選択して追加
    • 最初に追加した Fragment が自動で画面遷移の起点に設定される
      • "Assign start destination" で任意の Fragment に変更も可能
    • "Create new Destination" で Fragment の作成も可能
    • Fragment の代わりに placeholder の配置とそこへの Action も作成可能
      • placeholder への action をコードで呼び出す(後述)と例外になる
  3. 遷移元 Destination を選択、右側の丸印から遷移先の Destination にドラッグして Action を作成
  4. 作成した Action を使って NavController#navigate() で遷移

ダイアログへの遷移

  1. New Destination をクリックして DialogFragment を継承した Fragment を選択
  2. 配置した DialogFragment への Action を設定
    • XML をテキストで開くと <fragment> ではなく <dialog> で囲まれる
    • ダイアログから別の Destination への Action も作成可能
  3. 作成した Action を使って NavController#navigate() で開く

Global Action の実行

同じ Navigation graph 内からならどこからでも呼び出せる Action 。

  1. 任意の Destination でコンテキストメニューを開く
  2. "Add Action" > "Global" で作成(下記の短い矢印)
  3. 作成した Action を使って NavController#navigate で遷移

画面を戻るとき、 NavController ではなく FragmentManager からバックスタックを pop すると IllegalArgumentException が発生する

NavController で積んだバックスタックを FragmentManager で pop するのはNG。
戻った画面で再度同じアクションをすると、下記のような例外が発生する。

java.lang.IllegalArgumentException: navigation destination [APPLICATION-ID]:id/[ACTION-NAME] is unknown to this NavController

Fragment の pop は Navigation に任せるか、自分で操作したいときは NavController を使って行う。

データの受け渡し

サポートするデータ型

Destination が受け取るデータの定義

Navigation Graph で Destination を選択し、 Arguments の『+』ボタンを押下。
Destination が受け取りたいパラメータ名とデータ型をダイアログで入力。

項目名 意味
Name パラメータ名
Type データ型
Array 配列 or NOT
Nullable NULLの許容
Default Value デフォルト値(省略可)

Bundle を使う

  1. Bundle を生成し、Key にパラメータ名を、 Value に値をセット
  2. NavController#navigate() の第2引数にこの Bundle を指定して実行
  • 遷移元から渡す
val params = bundleOf("name" to "日本太郎", "age" to 30)
findNavController().navigate(R.id.actionToDestination, params)
  • 遷移先で受け取る
arguments!!.run {
    val name = getString("name")        // 日本太郎
    val age = getInt("age").toString()  // 30
}

Safe Args を使う

Safe Args を導入して任意の Action を作成すると、遷移元と遷移先それぞれに、データを受渡しする為のクラスと、必要なメソッドが自動で作成される。

Destination の種類 作成されるクラスのサフィックス 作成されるメソッド
遷移元 Directions アクションと同名のメソッド
遷移先 Args パラメータと同名の getter

実装例は以下の通り。

  • "name" と "age" という値を受け取る NextFragment を定義
  • NextFragment に遷移する StartFragment を定義
  • StartFragment → NextFragment に遷移する actionFirstToNext という Action を定義
  • 以下のクラスが生成され、それぞれに必要なメソッドが生成される
    • StartFragmentDirections クラス
      • actionFirstToNext(name, age) メソッド
    • NextFragmentArgs クラス
      • name ゲッタ
      • age ゲッタ

手順は以下の通り。

  1. 遷移元で Directions クラスの Action と同名のメソッドを使い Action オブジェクトを生成
  2. 生成した Action オブジェクトを引数にして NavController#navigate() を実行
  3. デリゲートを利用して Args クラスのインスタンスを生成
  4. 対応するプロパティを使ってパラメータを受け取る
  • 遷移元から送る
findNavController().navigate(
    StartFragmentDirections.actionFirstToNext("日本太郎", 30)
)
  • 遷移先で受け取る
// デリゲートで Args を作成
private val mNavArgs by navArgs<NextFragmentArgs>()

// 値を受け取る
val name = mNavArgs.name  // 日本太郎
val age = mNavArgs.age    // 30

その他 Tips

バックスタックをクリアして戻る

app:popUpToapp:popUpToInclusive="true" を組み合わせて定義。

要素 指定時の挙動
app:popUpTo 対象の Fragment から上にあるバックスタックを全て破棄して、次の Fragment を積む
app:popUpToInclusive app:popUpTo と併用
true だと app:popUpTo に指定した Fragment も含めてバックスタックを破棄して新たに Fragment を積む
false か指定しない場合、指定した Fragment は残したままで Fragment を積む

他の Navigation graph を利用する

<include> タグで、他の Navigation graph を指定

<include app:graph="@navigation/[OTHER-GRAPH-NAME]" />

画面遷移を起こせる OnClickListener を生成する

アクションの ID を引数に Navigation#createNavigateOnClickListener() を実行して OnClickListener を生成

// OnClickListener を生成してボタンに設定
button.setOnClickListener(
    Navigation.createNavigateOnClickListener(R.id.actionToDestination)
)

ダイアログ押下時のコールバックを受け取る

DialogFragment の parentFragmentManager.fragments.first() で呼び出し元の Fragment を取得できる

// 呼び出し元が DialogListener を実装している前提
AlertDialog.Builder(requireContext())
    .setPositiveButton("OK") { _, _ ->
        val listener = parentFragmentManager.fragments.first() as? DialogListener
        listener?.onDialog()
    }
    .create()

ダイアログのコールバックで NavController#navigate() するとエラー

ダイアログのコールバックを受け取る Fragment で NavController#navigate(R.id.actionFragmentToXXXXX) とするとエラーになる。

java.lang.IllegalArgumentException: navigation destination com.example.appid:id/actionFragmentToXXXXX is unknown to this NavController

リスナ経由で Action を呼び出すのは DialogFragment だが、呼び出そうとしてるのは Fragment の Action であって DialogFragment の Action ではない。
ダイアログから遷移先への Action を作成して、こちらを実行すればエラーにならない。

AlertDialog.Builder(requireContext())
    .setPositiveButton("OK") { _, _ ->
        findNavController().navigate(R.id.actionDialogToXXXXX)
    }
    .create()

参考

更新履歴

日時 更新内容
2020.02.22 FragmentManager からバックスタックを pop したときの例外について追記
2020.12.07 NavHostFragment を配置する にIDを指定しないと例外が起きることについて追記
73
73
1

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
73
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?