Skip to content

컴포즈‐Navigation 활용해보기

jimbo edited this page Jun 2, 2024 · 4 revisions

요약(Summary)

  • xml Fragment Navigation 과 동일한 compose Navigation 을 사용하여 간단한 페이지 흐름에 대해서 익힙니다.

계획(Plan)

  • 여러 화면들을 간단히 구성하고 화면마다 데이터를 전달합니다.
  • Compose 전용 Glide Image Loader를 만들어 봅니다.
  • 화면 이동시 유지보수측면에서 나은 방식이 있는지 고민합니다.

구현(Implements)

화면 전환

  • NavGraphBuilder 형태를 분석하면 url 방식으로 데이터를 전달합니다. 즉, 흔한 방식으로 사용되는 Parcelable 형태로 객체를 역/직렬화 하는 방식이 아닌 문자열로 이루어진 url 로 데이터를 전달하는 방향으로 설계 되어 있습니다.
  • url 방식이다보니 path/{arguments1}/{arguments2} 방식으로 처리하는 경우 무조건 필수 인자로 처리해야 합니다. 만약 인자값이 nullable인 경우에는 해당 방식으로 처리할때 이슈사항이 있습니다.
  • Android 도큐먼트에 나와있는것처럼 필수인자값인경우에는 path/{requiredArgument} 필수가아닌 경우 path/optionArgument={optionArgument} 방식으로 하라고 나와 있습니다.
  • 또한, A->B 화면으로 이동할때는 상기 방식으로 데이터를 전달하고, 이전 화면에 데이터를 전달하려면 NavController previousBackStackEntry 를 사용하여 간단히 해결할수 있습니다.
  • 확장 함수로 만들어두면 좋을거 같아서 아래와 같이 만들어봤습니다.
inline fun NavController.prevPutBundle(
    predicate: SavedStateHandle.() -> Unit
) {
    val savedStateHandle = previousBackStackEntry?.savedStateHandle ?: return
    predicate.invoke(savedStateHandle)
}

Compose Glide ImageLoader

  • 현재 Glide에서 지원하는 이미지 라이브러리 추가합니다.
implementation('com.github.bumptech.glide:compose:1.0.0-beta01')
  • core 모듈에 공통으로 두면 좋을거 같아서 아래와 같이 코드를 추가 했습니다.
@OptIn(ExperimentalGlideComposeApi::class)
@SuppressLint("ModifierParameter")
@Composable
fun ImageLoader(
    imageUrl: String,
    contentScale: ContentScale = ContentScale.Crop,
    modifier: Modifier = Modifier,
) {
    GlideImage(
        model = imageUrl,
        contentDescription = null,
        modifier = modifier,
        loading = placeholder(ColorPainter(TilTheme.color.gray3Light)),
        failure = placeholder(R.drawable.ic_error),
        contentScale = contentScale
    ) { requestBuilder ->
        requestBuilder.diskCacheStrategy(DiskCacheStrategy.NONE)
    }
}

Glide 4.16.x ~ AppGlideModule reflect 이슈

  • compose 버전이 5.0.0-rc 버전이라 기존에 있던 버전 같이 올리는게 좋을거 같아서 같이 5.0.0 으로 올렸는데 이때 문제점이 발생했습니다.
  • 기존에 AppGlideModule 안에 있는 함수들이 호출이 안되는 이슈가 발생했습니다. 확인해본 결과 Glide 4.16.x 이후 부터 ksp 이슈로 인하여 reflect 를 잘못 하고 있는 이슈가 있었습니다..(일해라 구글..bumbitch)
  • 물론 해결방법이야 어떤 사람이 PR 을 날렸고 merge 까지한 상태입니다. 하지만 무슨 이유인지 릴리즈를 안해서 해결된 버전을 사용할수 없었습니다. 결국 4.13.2 에서 멈추고 compose 만 버전 올리는 방식으로 처리했습니다.
  • 참고 링크

설계 (Architecture)

NavGraph 설계

  • NavGraphBuilder 분석하여 공통으로 사용되는 것들과 개발에 필요한 부분들을 분리하여 공통 함수등을 설계했습니다.
  • path{requiredArgument} 로 Route URL 구성하면 여러가지 제약사항이 있어서 Optional 하게 Argument 구성해봤습니다.
enum class Screens(
    val destination: String,
    val arguments: List<NamedNavArgument> = listOf() // type == StringType 만 가능
) {
    LOGIN(
        destination = "login",
        arguments = listOf(
            navArgument("user_id") {
                type = NavType.StringType
                nullable = true
            },
            navArgument("user_pw") {
                type = NavType.StringType
                nullable = true
            }
        )
    ),
    MEMO(
        destination = "memo",
        arguments = listOf(
            navArgument("user_id") {
                type = NavType.StringType
            }
        )
    );

    fun getNavGraph(
        builder: NavGraphBuilder,
        content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
    ) {
        val route = StringBuilder(destination)
        if (arguments.isNotEmpty()) {
            route.append("?")
            route.append(arguments.joinToString("&") { "${it.name}={${it.name}}" })
        }
        return builder.composable(
            route = route.toString(),
            arguments = arguments,
            content = content
        )
    }

    /**
     * 화면에 정의된 Argument 스펙 기준으로 파라미터 셋팅해서 URL 형식으로 전달하는 함수
     * @param argumentsMap 다음 화면에 전달할 파라미터 데이터
     */
    fun getNavigation(
        argumentsMap: Map<String, Any?> = mapOf()
    ): String {
        val route = StringBuilder(destination)
        if (arguments.isNotEmpty()) {
            route.append("?")
            route.append(arguments.mapNotNull {
                val value = argumentsMap[it.name]
                if (value != null) {
                    "${it.name}=$value"
                } else {
                    null
                }
            }.joinToString("&"))
        }
        return route.toString()
    }
}
  • 상기 방식으로 화면들을 정의했을때 화면들의 경로 방식은 아래처럼 구성이 됩니다.
    • /login?user_id={user_id}&user_pw={user_pw}
  • 화면 전환을 할때 데이터가 필요한 경우 아래와 같이 간단히 처리할수 있습니다.
Screens.LOGIN.getNavigation(
    mapOf(
        "user_id" to id.value,
        "user_pw" to pw.value
    )
)

회고

  • 구글에서 지향하는 Navigation 방식이 뭔지 잘 알거 같았습니다. 이제 더이상 공식적으로 Object -> Bytes, Bytes -> Object 방식으로 하는 역/직렬화는 사용하지 말라는건가 싶기도 합니다.. 물론 꼼수로 인자값에 json 형식으로 변환해서 Object -> String, String -> Object 로 할수는 있습니다만, 뭔가 지금까지 자기들이 만든 Parcelable, Parcelize 를 배제한다는게 참..아이러니하다는 생각이 들었습니다...
  • UI 를 그리는 클라이언트 입장에서 xml CoordinatorLayout in CollapseToolbarlayout 를 잘 다루어야 합니다. compose 에도 이와 같은 기능을 하는게 있지만, 아직은 학습이 좀 부족해서 애매하게 스크롤을 하는 경우 뚝뚝 끊기는? 이슈가 있었습니다. 이후 한번더 공부해서 잘 다룰수 있도록 할 생각입니다.
Screen_recording_20240602_125527.mp4
Clone this wiki locally