Compose Navigation Argument 로 url 전달하기

이지훈·2023년 6월 27일
3
post-thumbnail
post-custom-banner

TL;DR

Compose Navigation에서 Argument 로 url 을 전달할때는 인코딩을 해줘야 한다.

서두

이전에 만들었던 과제 연습용 레포를 컴포즈로 변환하는 과정 중, 가장 해결하는데 오래 걸렸던 이슈와 해결 방법을 공유하고자 한다.

https://github.com/easyhooon/KakaoMediaSearchApp2

구현 요구 사항은 Paging3 API 를 통해 검색어에 맞는 결과를 리스트로 뿌리고, 아이템을 클릭했을 경우 아이템의 url 이용하여 웹뷰로 상세화면을 보여주는 것이었다.

컴포즈를 사용하면 이전에 리사이클러뷰를 사용할때처럼 Adapter 나 ViewHolder 등의 코드를 따로 작성 해줄 필요 없이 lazyColumn 을 이용하여 적은 양의 코드로 빠르게 구현할 수 있는 장점이 있다.

하지만 네비게이션을 통해 데이터를 전달하는 파트에서는 기존의 fragment-navigation 보다 신경 써줘야 할 것이 많았다.

문제 발생

url 을 그냥 stringType 의 인자로 넘길 경우 다음과 같은 에러가 발생하는 것을 확인할 수 있는데

FATAL EXCEPTION: main
Process: com.kenshi.kakaomediasearchapp2, PID: 21573
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/detail/http://moving-coding.tistory.com/26 } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0xa2f2e50e) route=video}

딥링크 기능을 사용하지 않는 앱인데 딥링크 리퀘스트가 뭐 맞지 않는다는 에러가 발생한다. 흠...

공식 문서 및 compose-samples 의 navigation 관련 코드를 참고해서 작성했기 때문에 코드의 틀린 점을 찾을 수 없어 문제의 원인을 찾는데 오랜 시간이 걸렸다.

문제 해결

문제의 원인은 compose-navigation 을 통해 url 을 전달할 경우, 별도의 과정이 필요한데 url을 인코딩하여 전달해줘야 한다는 것이었다.

https://stackoverflow.com/questions/68950770/passing-url-as-a-parameter-to-jetpack-compose-navigation

윗 글의 답변을 확인해보면

Navigation routes are equivalent to urls. Generally you're supposed to pass something like id there.
When you need to pass a url inside another url, you need to encode it:

val encodedUrl = URLEncoder.encode("http://alphaone.me/", StandardCharsets.UTF_8.toString())
navController.navigate("HistoryDetail/$encodedUrl")

Navigation 의 routes 는 url과 동등하기 때문에 url 내에 다른 url 을 전달 해야하는 경우엔 인코딩을 과정을 거쳐서 넘겨야 한다는 것이었다.

위에 에러메세지를 확인해보면 routes 와 url이 동등하다는 그 의미를 알 수 있는데, compose-navigation은 web 에서 url 을 통해 인터넷 자원을 식별하는 것처럼 앱의 내부적으로 정의된 route(경로) 를 식별하여 해당 화면으로 이동한다.

uri = android-app://androidx.navigation/이동할화면의route/{이동할 화면의 route}/{arguemnt}

이때 argument 가 url 인데 이를 인코딩을 해주지 않았기 때문에 잘못된 url이 만들어져 해당 url과 match 되는 navigation destination 을 NavGraph 에서 찾을 수 없다는 에러였다!

따라서 아래와 같이 코드를 수정하여 문제를 해결할 수 있었다.
(참고로 argument 를 받는 컴포저블 함수에서는 별도의 디코딩을 수행할 필요가 없다.)

NavHost.kt

@Composable
fun SearchNavHost(
    navController: NavHostController,
    searchQuery: String,
    blogs: LazyPagingItems<BlogItem>,
    videos: LazyPagingItems<VideoItem>,
    images: LazyPagingItems<ImageItem>,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Video.route,
        modifier = modifier,
    ) {
        composable(route = Blog.route) {
            BlogScreen(
                blogs = blogs,
                searchQuery = searchQuery,
                onClickSeeBlogDetail = { urlType ->
                    val encodedUrl = URLEncoder.encode(urlType, StandardCharsets.UTF_8.toString())
                    navController.navigateToDetail(encodedUrl)
                }
            )
        }
        composable(route = Video.route) {
            VideoScreen(
                videos = videos,
                onClickSeeVideoDetail = { urlType ->
                    val encodedUrl = URLEncoder.encode(urlType, StandardCharsets.UTF_8.toString())
                    navController.navigateToDetail(encodedUrl)
                }
            )
        }
        composable(route = Image.route) {
            ImageScreen(
                images = images,
                onClickSeeImageDetail = { urlType ->
                    val encodedUrl = URLEncoder.encode(urlType, StandardCharsets.UTF_8.toString())
                    navController.navigateToDetail(encodedUrl)
                }
            )
        }
        composable(
            route = SearchDetail.routeWithArgs,
            arguments = SearchDetail.arguments
        ) {
            SearchDetailScreen()
        }
    }
}

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) {
        popUpTo(
            this@navigateSingleTopTo.graph.findStartDestination().id
        ) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
    }

private fun NavHostController.navigateToDetail(urlType: String) {
    this.navigateSingleTopTo("${SearchDetail.route}/$urlType")
}

Destination.kt

interface SearchDestination {
    val route: String
    val title: String
}

object Blog: SearchDestination {
    override val route = "blog"
    override val title = "블로그"
}

object Video: SearchDestination {
    override val route = "video"
    override val title = "동영상"
}

object Image: SearchDestination {
    override val route = "image"
    override val title = "사진"
}

object SearchDetail: SearchDestination {
    override val route = "detail"
    override val title = "상세 화면"
    const val urlTypeArg = "url_type"
    val routeWithArgs = "$route/{$urlTypeArg}"
    val arguments = listOf(
        navArgument(urlTypeArg) { type = NavType.StringType}
    )
}

혼틈 CS)
url 와 uri 의 차이:
URI(Uniform Resource Identifier): 웹에 있는 자원을 식별하는 고유한 문자열
모든 URL은 URI지만 모든 URI는 URL이 아님
URI는 자원의 이름만 식별하는 경우도 있음

URL(Uniform Resource Locator): 인터넷 상의 특정 자원의 위치를 가리키는 주소. 즉 URL은 그 자원이 어디에 있는지까지 알려줌

예) "www.example.com" 은 URI 이지만 URL은 아님. 왜냐하면 그것은 웹사이트를 식별하지만 위치는 명시하지 않음. 반면에 "http://www.example.com"은 URL. 왜냐하면 http 프로토콜을 통해 웹사이트의 위치를 알려줌. 이는 동시에 URI 이기도 함. 왜냐하면 역시 웹사이트를 식별하기 때문

일반적으로 URL은 http 또는 https 와 같은 웹 프로토콜을 통해 접근 가능한 자원의 위치를 가리킴

참고)

홀릭스 Jetpack Compose 사용자 모임 질문 답변

https://stackoverflow.com/questions/68950770/passing-url-as-a-parameter-to-jetpack-compose-navigation

profile
실력은 고통의 총합이다. Android Developer
post-custom-banner

0개의 댓글