依存関係の設定
まず最初に、Jetpack Compose用のナビゲーションライブラリを依存関係に追加しておきましょう。
モジュールレベルの build.gradle.kts
ファイルのdependenciesブロックに次のコードを追加します。(バージョンはその時点での最新の安定版に設定することをオススメします)
build.gradle.kts(Module :app)
// Enable Navigation
implementation("androidx.navigation:navigation-compose:2.7.5")
ナビゲーションライブラリの最新の安定バージョンは、Android Developer公式ページから確認できます。
依存関係の設定が完了したら、Sync Now ボタンをクリックして同期を完了させておきましょう。
この時点で開発が行き詰まってしまうような難しいポイントは特にないかと思いますが、ライブラリのバージョンの設定によっては、他の依存関係との問題が生じてエラーを起こす可能性があるので、その点だけ注意しておくと良いかと思います。
念のため、この時点で一度ビルドして、問題なくアプリが起動できることを確認しておくのも良い手法です。
万が一、同期やビルドでエラーが発生した場合は、落ち着いてエラーメッセージを確認してエラーの原因を特定すれば大丈夫です。
現時点では、モジュールレベルの build.gradle.kts
ファイルしか手を付けていないわけですから、問題があるとしたらライブラリのバージョンや、compileSdk などの設定でほぼ間違いありません。
エラーメッセージをしっかり確認して、問題のある依存関係の設定をやり直すことで、この手のエラーはほとんど解決できるかと思います。
POINT!
- ・Jetpack Composeでナビゲーションを利用するには、まず依存関係の設定を行う!
- ・依存関係の設定が完了したら、Sync Nowボタンを押して同期を完了させておこう!
- ・万が一エラーが起こっても、落ち着いてエラーメッセージを確認すれば大丈夫!
スクリーン(画面)の作成
画面移動のナビゲーションを実装するには、当然ですがアプリの画面(スクリーン)を複数用意しておかなければなりません。
たとえば、画面Aから画面Bに移動するには、少なくともコンポーザブルAとコンポーザブルBが必要です。
この記事では、Home
、ScreenA
、ScreenB
の3つのコンポーザブルを用意し、それらを使って解説を進めていきます。
画面構成のイメージは下のイメージ図をご覧ください。
Home
画面から ScreenA
と ScreenB
に画面遷移することができ、それぞれの画面から Home
に戻れるといった、シンプルな構造になっています。
本来ならば、ScreenA
と ScreenB
は、全く異なるデザインにした方がどちらの画面に遷移したかが分かりやすくなって良いのですが、デザインを細かく作り込むとコードも複雑になってしまいます。
なので、今回はコンポーザブルに表示されるテキストと色に差をつけることで、ScreenA
と ScreenB
の違いを表現します。
今回はあくまでナビゲーションの解説がテーマですので、各コンポーザブルのコードのうち、ナビゲーションとは関係ない部分については特に触れません。
Home
コンポーザブルには ScreenA
と ScreenB
に遷移するためのボタンを配置し、ScreenA
には ScreenA
であることを示すテキストと Home
に戻るためのボタン、ScreenB
には ScreenB
であることを示すテキストと Home
に戻るボタンを用意しておきます。
ScreenA
と ScreenB
はほぼ同じ画面構成であるため、CommonScreen
コンポザーブルを用意し、それに渡す引数を変えることで表示されるテキストと色を変えることとします。
また、現時点ではまだ画面遷移するための関数は定義できませんが、各コンポーザブルには引数を受け取らず、何も返さない関数 () -> Unit
を受け取れるようにしておきます。
Home
コンポーザブルは次のように定義できます。
@Composable
fun Home(
toScreenA: () -> Unit,
toScreenB: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxSize()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = 32.dp)
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
modifier = Modifier
.size(50.dp)
)
Text(
text = "HOME",
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Button(onClick = toScreenA ) {
Text(text = "To ScreenA")
}
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = toScreenB ) {
Text(text = "To ScreenB")
}
}
}
ホーム画面であることを分かりやすくするためにアイコンなどを配置しているので、ややコードが多いように感じられるかもしれませんが、ナビゲーションに関わる部分は画面遷移のために配置している2つのボタンのみです。
そして次に、ScreenA
と ScreenB
の共通デザインを担当する CommonScreen
コンポーザブルのコードを示します。
@Composable
fun CommonScreen(
modifier: Modifier = Modifier,
screenName: String,
screenColor: Color,
toHome: () -> Unit
) {
Surface(
color = screenColor,
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
) {
Text(
text = "This screen is $screenName",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
)
ElevatedButton(
onClick = toHome,
modifier = Modifier
.padding(top = 16.dp)
) {
Text(text = "HOME")
}
}
}
}
@Preview(showBackground = true)
@Composable
fun CommonScreenPreview() {
CommonScreen(
screenName = "Test",
screenColor = MaterialTheme.colorScheme.primary
) { }
}
CommonScreen
では、引数を3つ受け取っています。(modifierを除く)
screenName
は、画面の名称をテキストとして表示するための文字列で、screenColor
は、画面に色を指定するための Color
型の色になります。
また、Home
に戻るための関数として、toHome
をパラメータに指定しています。
この CommonScreen
を元に、ScreenA
と ScreenB
は次のように定義することができます。
@Composable
fun ScreenA(
modifier: Modifier = Modifier,
toHome: () -> Unit
) {
CommonScreen(
modifier = modifier,
screenName = "ScreenA",
screenColor = MaterialTheme.colorScheme.primary,
toHome = toHome
)
}
@Composable
fun ScreenB(
modifier: Modifier = Modifier,
toHome: () -> Unit
) {
CommonScreen(
modifier = modifier,
screenName = "ScreenB",
screenColor = MaterialTheme.colorScheme.secondary,
toHome = toHome
)
}
この設定により、ScreenA
ならば “This screen is ScreenA” と表示され、画面のカラーはPrimary(デフォルトでは紫色)で表示されることになります。
一方、ScreenB
では表示テキストとカラーが変化するため、画面が遷移した時、どちらの画面(コンポーザブル)に遷移したかが一目で分かります。
これでナビゲーションを実装するための準備が整いました!
あとはルートとなるコンポーザブルを用意して、ナビゲーションに必要な NavController
と NavHost
と呼ばれるものを定義すれば、ナビゲーション(画面遷移)を実装することができます。
これらの手順については、次のセクションで解説を進めていきます。
POINT!
- ・当然だが、画面遷移の実装には複数の画面(コンポーザブル)を準備しておく必要がある!
- ・画面遷移を行う関数は、() -> Unit の形式で渡す(受け取れる)ようにしておこう!
- ・画面遷移を視覚的に確認しやすくするために、テキストや色に変化をつけておくと良い!
ルートを用意してナビゲーションを実装する
Home
、ScreenA
・ScreenB
を包括するコンポーザブルとして Root
コンポーザブルを作成し、画面遷移に必要不可欠な navController
を定義します。
ここまでで、コードは下のようになります。
@Composable
fun Root(modifier: Modifier = Modifier) {
// Create NavController
val navController = rememberNavController()
}
そして、Root
コンポーザブル内で、ナビゲーションをホストする NavHost
を定義します。このとき、navController
パラメータには先程定義したばかりの navController
を渡せばOKです。
現時点で、Root
コンポーザブルの NavHost
部分のコードは次のようになります。
// Create NavHost
NavHost(
navController = navController,
startDestination =
) {
}
NavHost
に渡す必要のあるもう一つのパラメータの startDestination
ですが、これはその名の通り、起点となる画面(コンポーザブル)の名称を文字列型で指定します。
今回のサンプルでは Home
コンポーザブルが起点画面となるので、それが分かる名称であれば何でも良いでしょう。
普通に、”HOME” というふうに文字列を直接入力しても良いのですが、今回はKotlinの enum class を利用して設定していきたいと思います。
Root
コンポーザブルの外で、次のようにナビゲーションを enum class
で定義します。
enum class Screens {
HOME,
SCREEN_A,
SCREEN_B
}
enum class
でナビゲーションの名称を指定することで、単純な文字の入力ミスを防げるようになるだけでなく、ナビゲーションを様々な型で扱えるようになることにより、コードに柔軟性が生まれます。
たとえば、ordinal
で数値(Int)型、name
で文字列(String)型として enum class
で定義された値を扱うことができます。
今回は、startDestination
パラメータに指定する文字列型として扱う必要があるので、name
プロパティを利用しましょう。
コードは次のようになります。
// Create NavHost
NavHost(
navController = navController,
startDestination = Screens.HOME.name
) {
}
あとは、NavHost
のスコープ内に、composable
を利用して各コンポーザブルをセッティングしていきます。
composable
の route
パラメータには、当該コンポーザブルの名称を渡します。startDestination
と同様に、enum class
で定義したものを name
プロパティで文字列に変換して渡す必要があります。
ここまでの手順でコードは次のようになります。
NavHost(
navController = navController,
startDestination = Screens.HOME.name
) {
composable(Screens.HOME.name) {
Home(
toScreenA = {},
toScreenB = {},
modifier = modifier
)
}
composable(Screens.SCREEN_A.name) {
ScreenA(toHome = {})
}
composable(Screens.SCREEN_B.name) {
ScreenB(toHome = {})
}
}
ここまで来れば完成まであと少しです!
このままでは各コンポーザブルに配置されたボタンを押しても何も起こらないので、ボタンが押されると画面遷移が起こるように、navController
の navigate
関数を指定したものを、各コンポーザブルに渡すようにします。
navigate
関数の route
パラメータには、画面の名称を文字列で渡す必要があるので、これまでと同じ手法で設定していきます。
コードは次のようになります。
NavHost(
navController = navController,
startDestination = Screens.HOME.name
) {
composable(Screens.HOME.name) {
Home(
toScreenA = { navController.navigate(Screens.SCREEN_A.name) },
toScreenB = { navController.navigate(Screens.SCREEN_B.name) },
modifier = modifier
)
}
composable(Screens.SCREEN_A.name) {
ScreenA(toHome = { navController.navigate(Screens.HOME.name)})
}
composable(Screens.SCREEN_B.name) {
ScreenB(toHome = { navController.navigate(Screens.HOME.name)})
}
}
これでJetpack Composeを使ったナビゲーションの実装ができました!
動作は下の動画のようになります。
ナビゲーションはこれで完成ですが、もうひと工夫加えたいという方のために、最後のセクションでは画面遷移をアニメーションにする方法をご紹介します。
POINT!
- ・ルートとなるコンポーザブルでnavControllerを定義し、NavHostのnavControllerに渡そう!
- ・startDestinationには、起点となるコンポーザブルのスクリーンの名称(文字列)を指定しよう!
- ・スクリーンの名称は enum classで管理すると何かと便利!
画面遷移のトランジション
画面遷移のトランジションを指定しない場合、デフォルトでは移動元の画面と移動先の画面の透明度が変化することで画面遷移が行われます。
動画編集を行ったことがある方であれば、『ディゾルブ(Dissolve)』の効果をイメージすると分かりやすいでしょう。シンプルで汎用性の高いトランジションですね。
地味ではありますが、何も指定しなくても自然な画面遷移にはなるので、特にこだわらない場合はデフォルトのままでも良いでしょう。
ただし、画面遷移の挙動をコントロールしたい場合は追加の設定が必要です。
ここでは、遷移先の画面がスライドイン・スライドアウトするように設定するシンプルな方法をご紹介します。
1:左右からスライドイン・スライドアウトさせる
composable()
の enterTransition
と exitTransition
パラメータに、値の指定を追加します。
左右のスライドは slideIn(Out)Horizontally
を指定すればOKです。
この時、左と右のどちらの方向からスライドさせるかによって、initialOffsetX
と targetOffsetX
の値の指定が変わります。
画面遷移のトランジションに関係するコードは次の通りです。(トランジションは300ミリ秒に指定しています)
composable(
Screens.SCREEN_A.name,
enterTransition = {
slideInHorizontally(
animationSpec = tween(300),
initialOffsetX = { -it } // from left
)
},
exitTransition = {
slideOutHorizontally(
animationSpec = tween(300),
targetOffsetX = { -it }
)
}
) {
ScreenA(toHome = { navController.navigate(Screens.HOME.name) })
}
composable(
Screens.SCREEN_B.name,
enterTransition = {
slideInHorizontally(
animationSpec = tween(300),
initialOffsetX = { it } // from right
)
},
exitTransition = {
slideOutHorizontally(
animationSpec = tween(300),
targetOffsetX = { it }
)
}
) {
ScreenB(toHome = { navController.navigate(Screens.HOME.name) })
}
2:上下からスライドイン・スライドアウトさせる
指定方法はほとんど同じですが、slideIn(Out)Horizontally
ではなく slideIn(Out)Vertically
になるのと、initial(target)OffsetX
ではなく initial(target)OffsetY
に変わります
コードは次のようになります。
composable(
Screens.SCREEN_A.name,
enterTransition = {
slideInVertically(
animationSpec = tween(300),
initialOffsetY = { -it } // from top
)
},
exitTransition = {
slideOutVertically(
animationSpec = tween(300),
targetOffsetY = { -it }
)
}
) {
ScreenA(toHome = { navController.navigate(Screens.HOME.name) })
}
composable(
Screens.SCREEN_B.name,
enterTransition = {
slideInVertically(
animationSpec = tween(300),
initialOffsetY = { it } // from bottom
)
},
exitTransition = {
slideOutVertically(
animationSpec = tween(300),
targetOffsetY = { it }
)
}
) {
ScreenB(toHome = { navController.navigate(Screens.HOME.name) })
}
画面遷移を左右・上下方向のスライドにすることができました!
アプリの画面構成が2〜3画面程度であれば、ナビゲーションを利用するよりも、条件分岐でコンポーザブルの表示を切り替えた方が楽で効率的な場合もありますが、それ以上の画面が必要な場合は素直にナビゲーションを利用した方が良いかと思います。
また、今回は画面遷移の方法だけに解説の的を絞ったので触れることができませんでしたが、たとえば共通のトップバーに現在表示されている画面(コンポーザブル)の名称を表示させたりなど、もう一歩踏み込んだことをしたければ、さらに複雑な手順が必要となります。
そのあたりは、また別の記事としてご紹介できればと思います!
POINT!
- ・画面遷移のトランジションを調整するには、enterTransitionとexitTransitionパラメータに値を設定する!
- ・左右のスライドは、slideIn(Out)Horizontally!
- ・上下のスライドは、slideIn(Out)Vertically!