compose navigation으로 값을 전달할 때 query 문이나 json 변환 등 다양한 방법을 사용할 수 있다.
그러나 이러한 방법들은 매우 귀찮고 헷갈리며, 필요없는 코드가 너무 많아지기 때문에 선뜻 접근하기가 쉽지 않았는데...
이번에 새로운 Compose Navigation 버전이 나왔다!
전달할 데이터를 kotlinx-serialization으로 직렬화하여 저장하고, 사용할 때 다시 역직렬화하여 Screen에 인자를 전달하는 방법이다.
사용법은 다음과 같다.
1. compose-navigation:2.8.0-alpha08, kotlinx-serialization 추가
[versions]
kotlin = "1.9.21"
kotlinSerialization = "1.6.0"
androidx-navigation-compose = "2.8.0-alpha08"
[libraries]
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" }
[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
2. build.gradle에 추가
plugins {
alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(libs.kotlin.serialization)
implementation(libs.kotlin.androidxNavigationCompose)
}
3. Screen 타입 생성
// 직렬화 annotation 필수
sealed interface ScreenType {
@Serializable
data object ScreenA : ScreenType
@Serializable
data class ScreenB(
val a: String = "",
val b: Int = 0,
val c: Float = 0f
) : ScreenType
}
4. NavHost, NavGraph 생성
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
TestNavigationTheme {
NavHost(
navController = navController,
startDestination = ScreenType.ScreenA
) {
composable<ScreenType.ScreenA> {
ScreenA(
// ScreenA에서 ScreenB로 가는 버튼을 누르면
// ScreenB data class의 객체를 만들어 전달한다.
onClickScreenBButton = { screenType: ScreenType.ScreenB ->
// 이 때, 내부에서 직렬화를 수행하여 argument를 저장한다.
navController.navigate(route = screenType)
},
)
}
// 전달할 데이터가 있을 경우 backStackEntry를 통해
composable<ScreenType.ScreenB> { navBackStackEntry ->
// data class를 역직렬화하여 객체를 가져온다.
val args = navBackStackEntry.toRoute<ScreenType.ScreenB>()
ScreenB(
args = args,
onClickScreenAButton = {
navController.navigate(
route = ScreenType.ScreenA,
// navOptions를 통해 backStack 제거
navOptions = navOptions {
popUpTo(route = ScreenType.ScreenA) {
inclusive = true
}
}
)
}
)
}
}
}
}
}
}
@Composable
fun ScreenA(
onClickScreenBButton: (ScreenType.ScreenB) -> Unit,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = {
onClickScreenBButton(
// ScreenB에 넘겨줄 데이터를 만들어서 전달
ScreenType.ScreenB(
a = "from ScreenA",
b = 1,
c = 2f
)
)
}
) {
Text(text = "To ScreenB")
}
}
}
@Composable
fun ScreenB(
args: ScreenType.ScreenB,
onClickScreenAButton: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = args.a)
Text(text = "${args.b}")
Text(text = "${args.c}")
Button(
onClick = onClickScreenAButton
) {
Text("To ScreenA")
}
}
}
}
실행 결과
해당 navigate 함수를 어떻게 직렬화하는지 내부를 살펴보면,
@MainThread
@JvmOverloads
public fun <T : Any> navigate(
route: T,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
// finalRoute에 직렬화하여 저장한다.
val finalRoute = generateRouteFilled(route)
// 직렬화된 코드를 DeepLink로 만들어서 navigate한다
navigate(
NavDeepLinkRequest.Builder.fromUri(NavDestination.createRoute(finalRoute).toUri()).build(), navOptions,
navigatorExtras
)
}
// 직렬화 저장 코드
@OptIn(InternalSerializationApi::class)
private fun <T : Any> generateRouteFilled(route: T): String {
val id = route::class.serializer().hashCode()
val destination = findDestinationFromRoot(id)
// throw immediately if destination is not found within the graph
requireNotNull(destination) {
"Destination with route ${route::class.simpleName} cannot be found " +
"in navigation graph $_graph"
}
return route.generateRouteWithArgs(
// get argument typeMap
destination.arguments.mapValues { it.value.type }
)
}
결국엔 해당 클래스를 직렬화하여 DeepLink로 만들고, navigate() 메서드에 딥링크를 전달하여 navigate한다.
또한, 다음과 같은 코드로 navArgument를 가져오는데
val args = backStackEntry.toRoute<ScreenType.ScreenB>()
내부를 살펴보면,
public inline fun <reified T> NavBackStackEntry.toRoute(): T {
val bundle = arguments ?: Bundle()
val typeMap = destination.arguments.mapValues {
it.value.type
}
// 역직렬화해서 가져온다.
return serializer<T>().decodeArguments(bundle, typeMap)
}
// 역직렬화 코드
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun <T> KSerializer<T>.decodeArguments(
bundle: Bundle,
typeMap: Map<String, NavType<*>>
): T = androidx.navigation.serialization.RouteDecoder(bundle, typeMap).decodeSerializableValue(this)
역직렬화해서 가져올 수 있다.
이번에 최신 Compose Navigation을 사용하면서 느낀점은 일단 굉장히 많은 코드와 복잡성을 줄일 수 있다는 것이었다.
예전에 query문을 사용하면서 argument를 넘겼을 때 경로를 작성하는 것이 매우 까다로웠으며, 특히 해당 경로를 String 타입으로 판별하는 것이 매우 맘에 들지 않았다. 또한, argument를 가지고 올 때 nullable하게 가져오는 것도 상당히 별로였다.
그러나 아직 alpha 버전이지만, 이번에 나온 Compose Navigation을 사용하면서 위의 불만사항들을 싹 해소시켜주었다. 정식버전이 나오면 또 어떻게 변할진 모르겠지만, 이전에 사용했던 것보단 더욱 좋은 방식으로 개선되었으면 한다.
[추가]
다음은 composable {} 할 때 backStackEntry를 사용하여 argument를 계속 추가하는게 귀찮은 분들을 위해(바로 나..) 작성해봤다.
inline fun <reified T : Any> NavGraphBuilder.composableWithArgument(
crossinline screen: @Composable (T) -> Unit,
) {
composable<T> { backStackEntry ->
val decodeArgument = backStackEntry.toRoute<T>()
screen(decodeArgument)
}
}
// 사용법
composableWithArgument<ScreenType.ScreenA> {
ScreenA(
onClickScreenBButton = { screenType: ScreenType.ScreenB ->
navController.navigate(route = screenType)
},
)
}
composableWithArgument<ScreenType.ScreenB> { args ->
ScreenB(
args = args,
onClickScreenAButton = {
navController.navigate(
route = ScreenType.ScreenA,
navOptions = navOptions {
popUpTo(route = ScreenType.ScreenA) {
inclusive = true
}
}
)
}
)
}
'Android > Android' 카테고리의 다른 글
Type com.example.domain.BuildConfig is defined multiple times (0) | 2023.06.14 |
---|---|
[Compose] Navigation으로 URL 넘길 때 주의사항 (0) | 2023.05.16 |
[Compose] Naver Map 현재 위치로 이동하기 (0) | 2023.04.08 |
[Android] Compose로 Naver Map 띄우기 (0) | 2023.04.05 |
[Android] Clean Architecture 모듈화 해보기 - 2 (0) | 2023.03.22 |