Android Navigation 2.8.0-alpha08 분석

Android Chen·2024년 8월 25일

안녕하세요. 취준생때 글을 자주 작성하다가 카카오쪽에 취업 후 만 2년이 조금 넘는 경력이 쌓이고 소홀했던 블로그에 오랜만에 포스팅을 해보려고 합니다.

androidx.navigation

  • 이 글을 보러 오신분들이라면 compose 또는 fragment의 navigation을 사용해보신 분들이라고 생각합니다. 이 글에서는 navigation에 대한 설명은 자세히 하지 않을 것이며 변경사항이 많은 2.8.0-alpha08 버전에 대해 알아보려고 합니다.

  • 먼저, 2.8.0-alpha08버전에 어떤것들이 변경되었는지 문서로 살펴보겠습니다.

  • Navigation Compose와 Navigation Kotlin DSL에서 Safe Argument를 사용할 수 있다고 되어 있습니다.
  • Navigation Compose를 사용해보신분들이라면 아시겠지만, composable의 Route를 String값으로 관리하다 보니 타입 안정성을 보장할 수 없고, 실수로 잘못된 값을 추가하는 경우 온전히 개발자의 책임이었는데요, 2.8.0 이후로 Safe Argument를 사용할 수 있다고 합니다.
  • Kotlin Serialization을 이용하여 Navigation의 Route를 설정할 수 있다고 하네요. 우선 Navigation Compose 예시코드 먼저 보겠습니다.
  // Define a home destination that doesn't take any arguments
  @Serializable
  object Home

  // Define a profile destination that takes an ID
  @Serializable
  data class Profile(val id: String)

  // Now define your NavHost using type safe objects
  NavHost(navController, startDestination = Home) {
      composable<Home> {
          HomeScreen(onNavigateToProfile = { id ->
              navCeontroller.navigate(Profile(id))
          })
      }
      composable<Profile> { backStackEntry ->
          val profile: Profile = backStackEntry.toRoute()
          ProfileScreen(profile)
      }
  }
  • @Serializable 어노테이션이 붙은 object, data class를 composable의 Route로 받을 수 있도록 구현되어 있네요. 그리고 NavBackStackEntry 객체에서 직접 arguments?.getString()과 같이 argument를 꺼내지 않고, toRoute()라는 확장함수를 제공하여 그대로 Profile 객체 형태로 타입 안정성을 지키면서 사용할 수 있게 되었습니다.
    그렇다면 이렇게 변경된 composable 제네릭 함수의 내부도 한번 살펴보겠습니다
public inline fun <reified T : Any> NavGraphBuilder.composable(
    typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
    deepLinks: List<NavDeepLink> = emptyList(),
    noinline enterTransition:
        (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
            EnterTransition?)? =
        null,
    noinline exitTransition:
        (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
            ExitTransition?)? =
        null,
    noinline popEnterTransition:
        (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
            EnterTransition?)? =
        enterTransition,
    noinline popExitTransition:
        (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
            ExitTransition?)? =
        exitTransition,
    noinline sizeTransform:
        (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
            SizeTransform?)? =
        null,
    noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
)
  • 뭔가 많이 변경됐는데요 먼저 제네릭으로 받은 타입의 클래스의 타입을 코틀린 리플렉션 KClass형태로 전달하여 이것을 Route로써 사용하고 있습니다.

  • 그럼 ComposeNavigatorDestinationBuilder에 어떤 생성자가 추가됐는지 보겠습니다.

	// 기존 생성자
    public constructor(
        navigator: ComposeNavigator,
        route: String,
        content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
    ) : super(navigator, route) {
        this.composeNavigator = navigator
        this.content = content
    } 
    
    // 새롭게 추가된 생성자
    public constructor(
        navigator: ComposeNavigator,
        route: KClass<*>,
        typeMap: Map<KType, @JvmSuppressWildcards NavType<*>>,
        content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
    ) : super(navigator, route, typeMap) {
        this.composeNavigator = navigator
        this.content = content
    }
  • 기존에 route로 String 값을 받는 생성자가 존재하고, KClass<*> 형태로 받는 생성자가 추가됐네요
  • 그리고 처음보는 typeMap이라는 파라미터가 보이는데 한번 주석을 읽어보겠습니다.
  • map of destination arguments' kotlin type KType to its respective custom
  • 간단히 보자면 커스텀 NavType을 사용하고 싶은 경우에 사용하라고 되어있습니다. 위 예시에서는 Destination의 객체가 단순히 Primitive Type만을 사용하고 있거나 Object 형태로 되어있는데요,
  • 만약 다른 객체를 Destination의 파라미터로 사용하고 싶다면 어떻게 해야할까요? 예시를 보겠습니다.

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}
  • 만약 위와같이 커스텀 타입을 정의하려면 NavType을 상속받은 객체를 구현하고, 위와 같은 메소드를 구현해야 합니다. 자세한 내용은 Custom NavType에 있습니다.
  • 그리고 typeMap에 이렇게 전달할 수 있겠네요
 typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
  • 저는 Navigation Kotlin DSL을 사용해보진 않았지만 Compose와 동일한 형태로 사용할 수 있다고 하네요

  • 혹시 Navigation Kotlin DSL 이나 Navigation Compose를 사용하시는 분들이라면 안정화 버전이 출시된 이후에 한번 사용해보는것도 괜찮을 것 같습니다

  • 다음엔 Navigation Kotlin DSL에 대해 포스팅해보겠습니다

profile
https://github.com/Userz1-redd

0개의 댓글