はじめに
この記事はフラー株式会社 Advent Calendar 2024 12日目の記事です
11 日目は@masaya82さんのUIコンポーネントをライブラリに頼ることについてでした
1. なぜ記事を書いたか?
Navigation ComposeのSafeArgs周りでいい記事が見つからなかったから
2. 検証環境(2024/12/09 現在)
UIフレームワーク: Jetpack Compose
Kotlin 2.1.0
minSDK 24
Android Studio Ladybug | 2024.2.1
macOS Sonoma 14.7
3. ゴール
MVP(Minimum Viable Product)でNavigation ComposeのSafe Argsを体験(実装)してみる
4. 下準備(2024/12/04 現在)
- ライブラリをインポートする(Kotlinのバージョンを上げるなどは省略)
- kotlin SerializationとNavigation Composeを入れる
(Project) build.gradle
plugins {
// Existing plugins
alias(libs.plugins.compose.compiler) apply false
// add: kotlin serialization
kotlin("jvm") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}
(Module) build.gradle
plugins {
alias(libs.plugins.compose.compiler)
// add: kotlin serialization
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
}
dependencies {
// add: kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// add: navigation compose
implementation("androidx.navigation:navigation-compose:2.8.4")
}
5. フォルダ構成
(今回 viewModelは実装していないが〇〇Screen
の箇所に格納する想定)
- com.example.navArgsTest
- navigation
- MyAppNavHost.kt
- Route.kt
- screen
- firstScreen
- FirstScreen.kt
- FirstScreenNavigation.kt
- secondScreen
- SecondScreen.kt
- SecondScreenNavigation.kt
- MainActivity.kt
- MyApp.kt
6. 処理の流れの概要
7. MainActivity
MainActivityはMyAppを参照するように実装
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
enableEdgeToEdge()
ComposeNavigationSafeArgsTestTheme {
MyApp()
}
}
}
}
8. MyApp
MyAppにnavControllerを定義
MyAppNavHostにnavControllerを渡すように実装
MyApp.kt
@Composable
fun MyApp() {
val navController = rememberNavController()
MyAppNavHost(navController)
}
9. MyAppNavHost
画面の格納をしているイメージ
NavHostの引数はnavControllerとstartDestination(どの画面が最初に来るか?)を渡す
firstScreen()
とsecondScreen()
はNavGraphBuilderの拡張関数
リファクタリングをしてこのような形としている
MyAppNavHost.kt
@Composable
fun MyAppNavHost(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = Route.FirstScreenRoute,
) {
// firstScreen
firstScreen(navController)
// secondScreen
secondScreen(navController)
}
}
10. Route
Routeを定義することによってタイポを減らす目的がある.
Route.kt
object Route {
@Serializable
object FirstScreenRoute
@Serializable
data class SecondScreenRoute(
val name: String,
val id: String
)
}
11. 画面の定義
First Screen
FirstScreen.kt
@Composable
fun FirstScreen(navController: NavController) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text(
"This is First Screen",
modifier = Modifier.padding(8.dp),
fontWeight = FontWeight.Bold
)
Button(
onClick = {
navController.navigateToSecondScreen(name = "John", id = "012345FF")
},
) { Text("Go To Second Screen") }
}
}
}
Second Screen
SecondScreen.kt
@Composable
fun SecondScreen(name: String, id: String, navController: NavController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "This is Second Screen", fontWeight = FontWeight.Bold)
Text(text = "name: $name")
Text(text = "id: $id")
Button(onClick = { navController.navigateToFirstScreen() }) {
Text(text = "Go To First Screen")
}
}
}
12. Navigation定義
ポイント
- Navigationをするにあたってnavigate先をJsonSerializableオブジェクトを指定する(今回の場合 Route.〇〇Route)
- 値を渡すときはJsonSerializableのdata classを定義しそこから値を渡す(SecondScreenNavigation参照)
-
backStackEntry.toRoute()
でそれぞれの値を取得して画面遷移時に渡す(SecondScreenNavigation参照)
FirstScreenNavigation.kt
fun NavController.navigateToFirstScreen() {
navigate(Route.FirstScreenRoute)
}
fun NavGraphBuilder.firstScreen(navController: NavController) {
composable<Route.FirstScreenRoute> {
FirstScreen(navController)
}
}
SecondScreenNavigation.kt
// navControllerの拡張関数(navigateのラッパーみたいなイメージ)
fun NavController.navigateToSecondScreen(name: String, id: String) {
navigate(Route.SecondScreenRoute(name = name, id = id))
}
// navGraphBuilderの拡張関数(画面定義のラッパーみたいなイメージ)
fun NavGraphBuilder.secondScreen(navController: NavController) {
composable<Route.SecondScreenRoute> { backStackEntry ->
val secondScreenRouteArgs: Route.SecondScreenRoute = backStackEntry.toRoute()
SecondScreen(secondScreenRouteArgs.name, secondScreenRouteArgs.id, navController)
}
}