初めに
Android Advent Calendar 2022の16日目の記事です。
今年自分が担当しているプロジェクトにJetpack Composeを導入しました!!
悪戦苦闘しながら、新規画面をComposeで書きつつも、既存の画面もComposeに置き換えてます。
そこで知見を得た、ComposeのNavigationについてお話ししています!
Navigation Compose
Composeで画面遷移をする際は、Navigation Composeを使用します。
Google公式が、ComposeのNavigationのpathway(= tutorial)を公開しているので、そちらも進めていただければ、理解できるかなと思います!
セットアップ
viewを構築するモジュール(今回はapp
モジュールとして進めます。)に、androidx.navigation:navigation-compose
を追加します。
android {
/* 中略 */
kotlinOptions {
jvmTarget = '1.8'
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
buildFeatures {
compose true
}
}
dependencies {
/* 中略 */
// Compose
implementation platform("androidx.compose:compose-bom:2022.10.00")
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material:material"
implementation "androidx.compose.material:material-icons-extended"
debugImplementation "androidx.compose.ui:ui-tooling"
// Navigation Compose
implementation "androidx.navigation:navigation-compose:2.5.3"
}
画面設定
NavController
Androidの画面遷移は、NavControllerを用いて、遷移する画面の状態やバックスタックにある画面の状態を管理します。
rememberNavControllerメソッドを使用して、NavController
をrememberAPIで監視できるようにします。
このオブジェクトは、状態ホイスティングの原則より、最上位のコンポーネントで定義し、全てのComponentからアクセスできるようにするのが、公式から推奨されています。
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppScreen()
}
}
@Composable
fun MyAppScreen() {
val navController = rememberNavController() // NavControllerを定義
MyApptheme {
// 画面
}
}
}
NavHost
NavController
を作成したら、NavHostを定義します。
NavHost
は、既存のNavigation Graph
に相当するもので、画面遷移の設定を定義できます。
引数のnavController
に先ほど定義したnavControllerを、startDestination
にActivity起動時に表示する初期画面を設定します。
lambda内で表示する画面の定義を行います。
class MainActivity: ComponentActivity() {
/* 中略 */
@Composable
fun MyAppScreen() {
val navController = rememberNavController() // NavControllerを定義
MyApptheme {
Scaffold(
bottomBar = { /* TODO: 画面遷移の操作をします */ }
) { padding ->
NavHost(
navController = navController,
startDestination = Route.First.name, // 初期表示する画面
modifier = Modifier.padding(padding)
) {
// 表示する画面の定義
}
}
}
}
private enum Route {
FIRST,
SECOND,
THIRD;
}
}
composable()
NavHost
のlambda内で表示する画面の定義するには、composableメソッドを使用します。
引数のroute
で、ルート名を定義します。このルート名を使用して画面遷移する先を決めます。
lambda内に表示する画面を定義します。
遷移先に渡したいデータがある場合は、arguments
を指定する必要があります。
route
に渡したい値の変数名を引数プレースホルダーとして追加し、arguments
で渡したい値の設定を行います。
arguments
には、navArgumentメソッドを使用して、NamedNavArgument
を渡してあげます。navArgument
のlambdaでは、渡したい値の型やnullかどうか、デフォルト値を設定できます。
値は、NavBackStackEntryから取得できるので、それを画面遷移先の画面に渡してあげると、値の受け渡しができます。
class MainActivity: ComponentActivity() {
/* 中略 */
@Composable
fun MyAppScreen() {
val navController = rememberNavController() // NavControllerを定義
MyApptheme {
Scaffold(
bottomBar = { /* TODO: 画面遷移の操作をします */ }
) { padding ->
NavHost(
navController = navController,
startDestination = Route.FIRST.name,
modifier = Modifier.padding(padding)
) {
// 画面1
composable(route = Route.FIRST.name) {
FirstScreen()
}
// 画面2
composable(route = Route.SECOND.name) {
SecondScreen()
}
// 画面3
composable(
route = "${Route.THIRD.name}/{isFirstOpen}", // 渡したい値がある場合は、routeに引数プレースホルダーを追加する
arguments = listOf(
navArgument("isFirstOpen") {
// 渡したい値の設定
type = NavType.BoolType
nullable = false
defaultValue = true
}
)
) { entry ->
// NavBackStackEntryから値を取得して、次の画面に渡す。
val isFirstOpen = entry.arguments?.getBoolean("isFirstOpen")
ThirdScreen(isFirstOpen ?: true)
}
}
}
}
}
@Composable
fun FirstScreen() {
}
@Composable
fun SecondScreen() {
}
@Composable
fun ThirdScreen(isFirstOpen: Boolean) {
}
private enum Route {
FIRST,
SECOND,
THIRD;
}
}
NavType
渡すことができる型については、今までのNavigationと同じものを扱えます。NavTypeのドキュメントに扱える型が載っています。
// Enumを渡したい場合
navArgument("enum") {
type = NavType.EnumType(Route::class.java)
}
// Serializableを渡したい場合
navArgument("serializable") {
type = NavType.SerializableType(Route::class.java)
}
// Parcelableを渡したい場合
navArgument("parcelable") {
type = NavType.ParcelableType(Route::class.java)
}
// Referenceを渡したい場合
navArgument("isFirstOpen") {
type = NavType.ReferenceType
}
画面遷移
navigate
画面遷移する場合は、navigateメソッドを使用します。
画面遷移するトリガーでnavigate
メソッドを呼び出すと、画面が切り替わります。
値を渡す場合は、先ほど設定したrouteの引数プレースホルダーに値を渡します。
navController.navigate("route名")
navController.navigate("route名/$渡す値")
NavBackStackEntry
NavController
のcurrentBackStackEntryAsState
メソッドで現在のNavBackStackEntry
をState型として取得します。
このオブジェクトから、現在のデスティネーションを見て、そのデスティネーション階層の親が今表示されている画面のルート名と一致するかどうかを確認します。
このロジックが、今表示されている画面かどうかの判定になります。
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination // 現在のデスティネーション
Route.values().forEach { screen ->
// 今表示されている画面かどうか
val selected = currentDestination?.hierarchy?.any { it.route == screen.name } == true
}
全体コード
Navigation Componentのコードを記載しておきます!
これで、画面遷移ができるはずです!
class MainActivity : ComponentActivity() {
/* 中略 */
@Composable
fun MyAppScreen() {
val navController = rememberNavController()
var isThirdScreenFirstOpen by remember { mutableStateOf(true) }
NavigationComposeAppTheme {
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Route.values().forEach { screen ->
val selected =
currentDestination?.hierarchy?.any { it.route == screen.name } == true
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(screen.name) },
selected = selected,
onClick = {
if (screen == Route.THIRD) {
navController.navigate("${screen.name}/$isThirdScreenFirstOpen")
isThirdScreenFirstOpen = false
} else {
navController.navigate(screen.name)
}
}
)
}
}
}
) { padding ->
NavHost(
navController = navController,
startDestination = Route.FIRST.name,
modifier = Modifier.padding(padding)
) {
composable(route = Route.FIRST.name) {
FirstScreen()
}
composable(route = Route.SECOND.name) {
SecondScreen()
}
composable(
route = "${Route.THIRD.name}/{isFirstOpen}",
arguments = listOf(
navArgument("isFirstOpen") {
type = NavType.BoolType
nullable = false
defaultValue = true
}
)
) { entry ->
val isFirstOpen = entry.arguments?.getBoolean("isFirstOpen")
ThirdScreen(isFirstOpen ?: true)
}
}
}
}
}
@Composable
fun FirstScreen() {
Text(
text = Route.FIRST.name,
modifier = Modifier.fillMaxSize()
)
}
@Composable
fun SecondScreen() {
Text(text = Route.SECOND.name)
}
@Composable
fun ThirdScreen(isFirstOpen: Boolean) {
Text(
text = if (isFirstOpen) {
"Hello World!!"
} else {
Route.THIRD.name
}
)
}
private enum class Route {
FIRST,
SECOND,
THIRD;
}
}
また、今回のサンプルコードをgithubに置いておきます! 参考までに🙏