使う際に見つけやすいようにまとめておきます。
基本的に 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 を含めた遷移は定義できない
- Navigation graph (XML) を作成
- res でコンテキストメニューを開く
- "New" > "Android Resource File"
- "Resource Type" に Navigation を選択、ファイル名を入力して「OK」
- Activity/Fragment に NavHost を配置
- <fragment>タグで配置
- NavHostFragment#create() で生成した NavHostFragment を FragmentTransaction で配置
- Navigation graph に Destination と Action を定義
- 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)" />
- <fragment> に付与するID。たとえ使わない場合でも、 IDを指定しないとその画面に遷移した途端に『java.lang.IllegalArgumentException: No view found for id...』でクラッシュする
- バックキーとバックスタックの制御を NavHost に任せるか。次項を参照
- 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
- 配置する Fragment のクラス名は class 要素 にセット
- 前項と同じ。バックキーとバックスタックの制御を NavHost に任せるか
- 前項と同じ。res/navigation にある Navigation graph
- 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() } } }
- false でもバックスタックは積まれるので、
コードで生成して配置
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) への遷移
- Destinations ペインの HOST が NavHostFragment を配置した Activity/Fragment であることを確認
- New Destinationをクリックして、任意の Fragment を選択して追加
- 最初に追加した Fragment が自動で画面遷移の起点に設定される
- "Assign start destination" で任意の Fragment に変更も可能
- "Create new Destination" で Fragment の作成も可能
- Fragment の代わりに placeholder の配置とそこへの Action も作成可能
- placeholder への action をコードで呼び出す(後述)と例外になる
- 最初に追加した Fragment が自動で画面遷移の起点に設定される
- 遷移元 Destination を選択、右側の丸印から遷移先の Destination にドラッグして Action を作成
- 作成した Action を使って NavController#navigate() で遷移
ダイアログへの遷移
- New Destination をクリックして DialogFragment を継承した Fragment を選択
- 配置した DialogFragment への Action を設定
- XML をテキストで開くと <fragment> ではなく <dialog> で囲まれる
- ダイアログから別の Destination への Action も作成可能
- 作成した Action を使って NavController#navigate() で開く
Global Action の実行
同じ Navigation graph 内からならどこからでも呼び出せる Action 。
- 任意の Destination でコンテキストメニューを開く
- "Add Action" > "Global" で作成(下記の短い矢印)
- 作成した 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 を使う
- Bundle を生成し、Key にパラメータ名を、 Value に値をセット
- 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 ゲッタ
- StartFragmentDirections クラス
手順は以下の通り。
- 遷移元で Directions クラスの Action と同名のメソッドを使い Action オブジェクトを生成
- 生成した Action オブジェクトを引数にして NavController#navigate() を実行
- デリゲートを利用して Args クラスのインスタンスを生成
- 対応するプロパティを使ってパラメータを受け取る
- 遷移元から送る
findNavController().navigate(
StartFragmentDirections.actionFirstToNext("日本太郎", 30)
)
- 遷移先で受け取る
// デリゲートで Args を作成
private val mNavArgs by navArgs<NextFragmentArgs>()
// 値を受け取る
val name = mNavArgs.name // 日本太郎
val age = mNavArgs.age // 30
その他 Tips
バックスタックをクリアして戻る
app:popUpTo
と app: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()
参考
-
Navigation Overview
https://developer.android.com/guide/navigation -
AndroidX Releases
https://developer.android.com/jetpack/androidx/versions
更新履歴
日時 | 更新内容 |
---|---|
2020.02.22 | FragmentManager からバックスタックを pop したときの例外について追記 |
2020.12.07 | NavHostFragment を配置する にIDを指定しないと例外が起きることについて追記 |