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