【Jetpack Compose/Android】NavControllerで画面遷移の実装方法!

【Jetpack Compose/Android】NavControllerで画面遷移の実装方法!

この記事からわかること

  • Kotlin/Android Jetpack ComposeNavigation Composeとは?
  • NavController使い方
  • 画面遷移方法
  • データ渡すには?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

これまでAndroidのナビゲーション(画面遷移)機能はFragmentを使用して画面を管理し、FragmentTransactionなどを通じて画面の切り替えを行うのが一般的でした。しかしJetpack ComposeからFragmentを使わずとも画面遷移や状態管理が可能になりました。今回はComposeのNavController(Navigation Compose)の使い方についてまとめていきます。

Jetpack Compose自体の基本的な使用方法に関しては以下の記事を参考にしてください。

Navigation Composeの導入

公式リファレンス:Compose でのナビゲーション

Composeのナビゲーション機能は「Navigation Compose」というライブラリにまとめられて管理されています。そのためNavigation Composeを使用するためには依存関係を追加する必要があります。最新バージョンは公式を確認してください。


dependencies {
    val nav_version = "2.9.3"

    implementation("androidx.navigation:navigation-compose:$nav_version")
    // 上記でSync Nowを実行した後に警告が出るので修正すると以下の記法に変換されlibs.versions.tomlで管理できます
    // implementation(libs.androidx.navigation.compose)
}

実際のサンプルコードもGitHubに挙げているので参考にしてください。

GitHub:Sample-Compose-NavController

NavControllerの使い方

NavController画面遷移を管理するコントローラーの役割を持っています。画面自体のインスタンスを保持しているわけではなく、表示中のルート情報やバックスタックなどを保持しています。

// NavController を作成・保持
val navController = rememberNavController()

NavControllerインスタンスはrememberNavControllerメソッドを使用して取得します。Composeでは状態が再コンポーズされると変数が初期化される特徴がありますがrememberNavControllerを使用することで再コンポーズしても状態を保持してくれるので画面遷移の情報が破棄されずに済みます。

実際に表示する画面(Compose)を管理するのはNavHostの役割です。NavController最初に表示する画面ルートを渡し、各ルートに紐づく画面(Compose)をcomposableメソッドを使用して定義します。

NavHost(
  navController = navController,
  // 最初に表示する画面ルート
  startDestination = Screen.Home.route
) {
    // ルートに紐づく画面(Compose)を定義
    composable(Screen.Home.route) { HomeScreen(navController) }
    composable(Screen.Detail.route) { DetailScreen(navController) }
    composable(Screen.Settings.route) { SettingsScreen(navController) }
}

ルート情報はenumなどにまとめておくと管理しやすいです。

enum class Screen(val route: String) {
    Home("home"),
    Detail("detail"),
    Settings("settings")
}

画面遷移を実行する

実際に画面を遷移させるにはnavController.navigate(ルート名)を使用します。

// 詳細画面へ移行する
navController.navigate(Screen.Detail.route)

画面遷移が実行されるとNavController内に表示していた画面ルート(バックスタック)が記録され、popBackStackメソッドを使用することでその画面に戻ることが可能になります。

// 前の画面に戻る
navController.popBackStack()

画面遷移時にデータを渡す

画面遷移を行う際に任意のデータを遷移先の画面に渡すことも可能になっています。ただこれは「遷移元 => 遷移先」 or 「遷移先 => 遷移元」で少し処理が異なります。

遷移元 => 遷移先」へ画面遷移する際に任意のデータを渡すにはrouteとして渡す文字情報にルート名/{パラメータ名}のように記述します。ルート情報の定義クラスを以下のようにしておくと管理しやすくなります。

sealed class Screen {
    abstract fun route(): String

    data object Home : Screen() {
        override fun route() = "home"
    }

    data object Settings : Screen() {
        override fun route() = "settings"
    }
    // データを渡せる構造にしておく
    data object Detail : Screen() {
        const val ARG_ITEM_ID = "itemId"
        /** route pattern */
        override fun route() = "detail/{$ARG_ITEM_ID}"
        /** 画面遷移する際はこちらを使用する */
        fun route(itemId: Int) = "detail/$itemId"
    }
}

NavHostで指定する画面(Compose)側ではcomposableメソッドの引数argumentsにコレクション型で渡したいパラメータを指定します。navArgument{パラメータ名}を指定しさらに渡されるデータタイプをNavType型で指定します。

composable(
    route = Screen.Detail.route(),
    // ナビゲーションの引数として渡されるパラメータ
    arguments = listOf(
      // パラメータを含んだルートとタイプ
      navArgument(Screen.Detail.ARG_ITEM_ID) { type = NavType.IntType }
    )
) { backStackEntry ->
    // backStackEntry.argumentsからルートのパラメータを取得する
    val itemId = backStackEntry.arguments?.getInt(Screen.Detail.ARG_ITEM_ID) ?: 0
    DetailScreen(itemId = itemId, onBack = { navController.popBackStack() })
}

backStackEntry.argumentsからルート情報を取得することができるのでgetIntでパラメータを取得して、画面に渡せば完了です。パラメータが存在しないとjava.lang.IllegalArgumentException: Cannot set route "detail" for destination Destination(0x0). Following required arguments are missing: [itemId]のようなエラーが出てクラッシュするので注意してください。

遷移先から遷移元にデータを渡す

遷移先 => 遷移元」へ画面遷移する際に任意のデータを渡すにはSavedStateHandlepreviousBackStackEntryを使用します。

遷移先の実装ではpreviousBackStackEntryからSavedStateHandleを取得しsetメソッドを使ってキーバリュー形式で値を保存します。


Button(
    onClick = {
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("backScreen", "詳細画面から戻ったよ")
        navController.popBackStack()
    }
) {
    Text("Back")
}

遷移元ではSavedStateHandleを取得しgetStateFlow/getLiveDataメソッドを使用してキーを指定することでStateFlow/LiveData型で取得できます。これをState型に変換しvalueから渡したデータを参照することができます。initialValueで初期値を指定することができますがNullableにしたい場合は明示的にgetStateFlow<String?>のように型を指定しておかないとエラーになるので注意してください。


val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val backScreen: String? = savedStateHandle
    ?.getStateFlow<String?>("backScreen", initialValue = null)
    ?.collectAsState()
    ?.value

if (!backScreen.isNullOrEmpty()) {
    Text(backScreen)
}

渡せるデータ型

画面遷移時に渡すことができるデータには制限があります。可能なのは「プリミティブ型」、「リスト(プリミティブ)型」、「オブジェクト型(※)」、「リファレンス型」になります。「遷移元 => 遷移先」の場合にはNavTypeで明示的に型を指定する必要があるのでそれぞれ定義されています。

NavType.StringType
NavType.IntArrayType
NavType.SerializableType(clazz: Class<T>)
NavType.ReferenceType
etc...

※ オブジェクト型に関してはParcelable / Serializableを継承しているオブジェクトのみがデータ受け渡し対象になります。

アクティブな画面ルート情報を取得する

アクティブな画面ルート情報を取得するにはcurrentBackStackEntryAsStateを使用します。State型で取得することでナビゲーションが変化した時に自動的に再コンポーズしてくれるので常に最新の情報を取得してくれます。

val currentDestination = navController
  .currentBackStackEntryAsState().value?.destination

タブバーを実装する際にバックスタックを適切に管理する

アプリでよくある下部のタブバーをNavControllerで実装する場合はバックスタックの取り扱いに注意する必要があります。タブのタップの際にnavigateだけを呼び出しているとどんどんバックスタックが蓄積していってしまいます。これを防ぐためには以下のように実装します。

Scaffold(
    bottomBar = {
        NavigationBar {
            AppScreen.Tab.entries.forEach { tab ->
                // 最新の画面ルート情報を取得(変化したら再コンポーズされる)
                val currentDestination = navController
                    .currentBackStackEntryAsState().value?.destination

                NavigationBarItem(
                    // 選択状態は現在のタブ状態で識別
                    selected = currentDestination?.route == tab.route(),
                    onClick = {
                        navController.navigate(tab.route()) {
                            // バックスタックが無限に積み上がらないように制御
                            popUpTo(navController.graph.findStartDestination().id) {
                                // popUpToで消された画面の状態を保存しておく
                                saveState = true
                            }
                            // 今いる画面と同じルートに遷移しようとしたら、新しいインスタンスを積まずに再利用する
                            launchSingleTop = true
                            // saveStateで保存された状態があればそれを復元する
                            restoreState = true
                        }
                    },
                    label = { Text(tab.title) },
                    icon = { /* アイコンを追加したければここに */ }
                )
            }
        }
    }
)

popUpTo

// バックスタックが無限に積み上がらないように制御
popUpTo(navController.graph.findStartDestination().id) {
    // popUpToで消された画面の状態を保存しておく
    saveState = true
}

popUpTo指定したルートまでバックスタックを戻るメソッドです。ここではnavController.graph.findStartDestination()バックスタックの一番最初を取得し戻しています。そしてsaveStateではpopUpToで消された画面の状態を内部的に保存しておくことができます。

launchSingleTop

launchSingleToptrueにしておくことで今いる画面と同じルートに遷移しようとしたら、新しいインスタンスを積まずに再利用することが可能です。

// 今いる画面と同じルートに遷移しようとしたら、新しいインスタンスを積まずに再利用する
launchSingleTop = true

restoreState

restoreStatetrueにしておくことでsaveStateで保存された状態があればそれを復元することが可能です。

// saveStateで保存された状態があればそれを復元する
restoreState = true

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index