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을 인코딩하여 전달해줘야 한다는 것이었다.
윗 글의 답변을 확인해보면
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/{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 사용자 모임 질문 답변