Jetpack Compose入門(15) 画面遷移
Jetpack Compose入門は今回から実践編に入ります。実践編では、一般的なアプリを開発するうえで必要なポイントを押さえていきます。
今回は画面遷移について学びます。前回までの基本編では、一画面の中で基本的なUIを作成する方法を学びました。しかし実際のアプリは一画面で完結することは稀です。複数の画面で構成されるアプリでは、画面遷移を実装する必要があります。
Navigation Compose
Jetpack Composeアプリで画面遷移を実現するには、Navigation Composeを使うのが簡単です。Navigation Composeパッケージでは、コンポーザブル同士の関係を定義することによって、コンポーザブルで作成した複数の画面を、一つのアクティビティ内で切り替えることができます。定義する内容は従来のNavigationと同じで、画面を定義し、どの画面からどの画面に遷移するのかといった関係を追加していきます。
簡単な画面遷移
遷移する画面の作成
最初に以下の2つの画面を作成し、ボタンをクリックしたら画面を遷移するようにします。ソースコードは、サイズやパディングなどの調整は省略して、UIの構造だけが分かるように抜き出したものを掲載しています。見た目のカスタマイズは基本編をご覧ください。
@Composablefun Screen1(onClickButton: ()->Unit = {}) { Column { Text(text = "Screen 1") Button(onClick = onClickButton) { Text(text = "Go to Screen 2") } }}
@Composablefun Screen2(onClickButton: ()->Unit = {}) { Column { Text(text = "Screen 2") Button(onClick = onClickButton) { Text(text = "Back to Screen 1") } }}

依存関係の追加
Navigation Composeを使うには、androidx.navigation.composeパッケージが必要です。build.gradle(app)に依存関係を追加します。最新のバージョン番号はリリースノートで確認できます。2022/1/12時点の安定版は2.3.5ですが、Navigation Composeは2.4.0から追加されますので、最新版は2.4.0-rc01です。
dependencies { implementation("androidx.navigation:navigation-compose:2.4.0-rc01")}NavHostの作成
画面遷移の実装は、以下のようになります。 Navigation Composeのコードは、すべてComposableの中に実装します。ここではsetContent()の直下に実装していますが、必ずしも同じ場所でなくても構いません。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val navController = rememberNavController() NavHost(navController = navController, startDestination = "screen1") { composable(route = "screen1") { Screen1(onClickButton = { navController.navigate("screen2") }) } composable(route = "screen2") { Screen2(onClickButton = { navController.navigateUp() }) } } }}初めにrememberNavController()でNavControllerのインスタンスを作成します。(正確には、NavControllerのサブクラスのNavHostControllerです。)rememberがついていることから分かるように、NavControllerは状態を持ちます。画面遷移イベントが発生するとNavControllerの状態が変化し、これをトリガに再コンポーズされ、表示が更新されるという仕組みになっています。
次にNavHost()で画面遷移に関連するコンポーザブルを定義します。
@Composablefun NavHost( navController: NavHostController?, startDestination: String?, modifier: Modifier? = Modifier, route: String? = null, builder: (@ExtensionFunctionType NavGraphBuilder.() -> Unit)?): UnitnavController引数には、さきほど作成したNavControllerを渡します。
NavHost()の直後のラムダ式はNavHost()関数のbuilder引数です。このラムダ式内でcomposable()関数を使って画面遷移に関連するコンポーザブルを登録します。コンポーザブルの識別子は、routeという引数でcomposable()に指定します。
NavHost()のもう一つの必須引数のstartDestinationには、いずれか一つのコンポーザブルのrouteを指定します。ここで指定したrouteのコンポーザブルが、最初に表示される画面になります。
画面遷移する
特定の画面に遷移するには、NavController#navigate()を呼び出します。引数には遷移先のrouteを文字列で指定します。上の例では、Screen1のonClickButtonイベントでscreen2に遷移する処理を書いています。
Screen1(onClickButton = { navController.navigate("screen2") })前の画面に戻る
遷移先の画面から遷移元の画面に戻るには、NavController#navigateUp()を呼び出します。上の例では、Screen2のonClickButtonイベントで遷移元(Screen1)に戻る処理を書いています。
Screen2(onClickButton = { navController.navigateUp() })なお、NavControllerにはpopBackStack()という関数もあります。一見navigateUp()と同じように感じますが、popBackStack()は端末の戻るボタン(Android12なら画面端からのスワイプ操作)と同じ動作になります。すなわち、戻り先が別のアプリになる可能性があります。例えば、Screen2が別のアプリからのインテントを受け取って起動した場合、戻るボタンやpopBackStack()の戻り先は、インテントを発行したアプリになります。一方でnavigateUp()はあくまでアプリ内での戻る動作になります。
実装場所
上の例では、コンポーザブル階層の最上位(setContent()の直下)でNavControllerとNavHostを作成しています。実際には必ずしもこの場所でなくてもよいですが、画面遷移に関連するすべてのコンポーザブルがNavControllerを必要とするので、必然的に上の方の階層で作成することになります。
また実際の画面遷移処理に相当するnavigate()やnavigationUp()は、上の例ではNavHostのラムダ引数内に書いています。そのために下位コンポーザブルからUIイベントを受け取るようにしています。別の方法として、NavControllerを下位コンポーザブルに渡し、下位コンポーザブル内でnavigate()などの処理を書くことも可能です。ただ、上の例のようにNavHost()の中に画面遷移をまとめて書くと、どの画面からどの画面に遷移するのかの見通しがよくなるのでお勧めです。
結果
以上で最も簡単な画面遷移を実現することができました。実行するとこんな感じになります。

引数を渡す
移動先の画面にパラメータを渡したい場合は、composable()のrouteにパラメータを追加します。
引数を受け取る側
引数を受け取る側のcomposable()では、引数名を{ }で囲って、/で区切ってrouteの後ろに追加します。さらに、argument引数を使って引数の型を定義します。以下の例では、String型のtext引数とInt型のid引数を定義しています。使用できる型の一覧は、NavTypeに定義されています。
受け取った引数は、composable()のcontent引数(ラムダ式の部分)で、NavBackStackEntryオブジェクト経由で取得します。
composable( route = "screen2/{text}/{id}", arguments = listOf( navArgument("text") { type = NavType.StringType }, navArgument("id") { type = NavType.IntType } )) { backStackEntry -> val text = backStackEntry.arguments?.getString("text") ?: "" val id = backStackEntry.arguments?.getInt("id") ?: 0 ...}引数を渡す側
引数を渡す側では、navigate()のroute引数の文字列に引数を/で区切って埋め込みます。上記で定義したscreen2にtextとidを渡す例は以下のようになります。
navController.navigate("screen2/$text/$id")サンプルコード
最初のサンプルを改造して、Screen1でクリックしたボタンに応じた文字列と数値をScreen2に渡して表示するようにしてみました。
NavHostの実装は以下のようになります。
setContent { val navController = rememberNavController() NavHost(navController = navController, startDestination = "screen1") { composable(route = "screen1") { Screen1 { text, id -> navController.navigate("screen2/$text/$id") } } composable( route = "screen2/{text}/{id}", arguments = listOf( navArgument("text") { type = NavType.StringType }, navArgument("id") { type = NavType.IntType }, ) ) { backStackEntry -> val text = backStackEntry.arguments?.getString("text") ?: "" val id = backStackEntry.arguments?.getInt("id") ?: 0 Screen2(text, id) { navController.navigateUp() } } }}各画面の実装は以下のようになります。
@Composablefun Screen1(onClickButton: (String, Int)->Unit = { _,_ -> }) { Column { Text(text = "Screen 1") Button(onClick = { onClickButton("Good morning!", 1) }) { Text(text = "(1) Good morning!") } Button(onClick = { onClickButton("Good afternoon!", 2) }) { Text(text = "(2) Good afternoon!") } }}
@Composablefun Screen2(text: String = "text", id: Int = 0, onClickButton: ()->Unit = {}) { Column { Text(text = "($id) $text") Button(onClick = onClickButton) { Text(text = "Back to Screen 1") } }}

結果
これでScreen1からScreen2にパラメータを渡すことができました。実行結果はこんな感じになります。

まとめ
今回は、Jetpack ComposeでNavigationによる画面遷移を実現する方法を紹介しました。単純な画面遷移に加えて、画面間でパラメータを受け渡しする方法も説明しました。Navigationを使うことによって、画面同士の関係を一か所にまとめて記述することができ、全体のUI構造の見通しがよくなることが期待できます。
パラメータの受け渡しについては、今回紹介した方法ではStringやIntなどの決まった型しか対応できません。文字列ベースで受け渡しを行うというのも、あまりスマートではないなあと個人的には思います。そこで、自作のクラスオブジェクトを受け渡ししたい場合や、多くのパラメータを受け渡ししたい場合は、ViewModel経由で受け渡しを行うのが一般的です。
さらに、遷移先画面から遷移元画面に戻るときに、onActivityResult()のように結果を受け取りたいというケースもあると思いますが、これもJetpack ComposeではViewModel経由で行うのが一般的です。ViewModelとJetpack Composeの連携については今後紹介する予定です。
