ANDROID/Android Compose

[Android/Compose] Type Safety한 Navigation 쉽게 사용하기

주 녕 2024. 11. 30. 22:15
반응형

Compose는 비교적 새로운 기술이어서인지 나날이 발전해가는게 눈에 보이는 라이브러리인 것 같다

초기 버전부터 사용하다보니 왜 없지? 하는 기능도 상당히 많은 편이었는데, 그만큼 꾸준히 발자취를 쫒아야 하는...

(열심히... 하겠습니다)

 

 

 

 

 

 

그 중 제일 불편(?)하고 구조 만들때 항상 고심하게하던 라이브러리였던 Navigation이 업데이트 되었다는 소식

 

Compose를 사용한 탐색  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose를 사용한 탐색 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 탐색 구성요소는 Jetpack 지원을 제

developer.android.com

 

요즘 갑자기 문상훈 짤에 빠져버렸다.

 

 

 

이번 업데이트 사항의 핵심은 Destination을 정의하는 방법과 argument를 넘기는 방법을 보면 알 수 있을 것이다.

그 외의 방법들은 기존과 거의 동일하다.

하지만 항상 개발해도 까먹는 까마귀는 기록을 남겨야겠다.

 

 

 

1. NavHost, NavController 만들기

✨ 이전 포스팅에서 기본 개념과 만드는 방법을 확인할 수 있다.

 

[Android/Compose] Navigation 기억하기

왜인지 모르겠지만 Navigation은 항상 헷갈리는 토픽 중 하나였다이번 포스팅으로 Navigation의 구조이 대해서 좀 더 이해 + 기억할 수 있으면 좋겠다  Navigation의 주요 구성    NavHost (현재 Destination

junyoung-developer.tistory.com

이 글을 읽어보면 알겠지만 NavHost가 navigation Graph를 만들어준다.

 

 

 

2. Destination을 @Serializable로 정의하기*

이 부분이 이번 글의 핵심이 아닐까 싶다!

Compose의 이전 Navigation

  • Destination과 화면 간 전달한 데이터의 key는 String 타입
    • Destination 관리의 어려움
    • 화면 간 데이터를 전달해야 할 때, argument key 관리의 어려움

Destination 이름이 겹치거나 argument key를 잘못 전달했어도 알아내기 쉽지 않았다. (Runtime에 Graph가 생성되기 때문)

그래서 회사에서 진행 중인 프로젝트에서는 별도의 Enum Class와 Extension 함수를 만들어서 관리했었다.

 

현재 Compose의 Navigation

  • Destination은 object 혹은 data class (화면 간 전달할 데이터가 필요한 경우)
  • @Serializable 어노테이션을 붙여 자동으로 serialization, deserialization가 되도록 함
@Serializable
object Destination1

@Serializable
data class Destination2(val note: String)

 

왜 Serializable을 사용했을까?

 

서버와 API를 통해 통신해봤다면 @Serializable 어노테이션은 한번쯤 사용해봤을 것 같다.

공식문서에서는 Serialization을 다음과 같이 설명하고 있다.

Serialization is the process of converting data used by an application to a format that can be transferred over a network or stored in a database or a file. In turn, deserialization is the opposite process of reading data from an external source and converting it into a runtime object. Together, they are essential to most applications that exchange data with third parties

 

  • Serialization : 응용 프로그램에서 사용하는 데이터를 네트워크를 통해 전송하거나 DB나 파일에 저장할 수 있는 형태로 변환
  • Deserialization : 외부 소스에서 데이터를 읽고 runtime 객체로 변환

https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/

 

String, ByteArray, HextString으로 인코딩, 디코딩하는 함수가 있는 것을 확인할 수 있다.

 

기존 String으로 관리하던 Route와 Augument를 object와 data class로 관리하면서

내부적으로 이 데이터를 전달하고 처리하는 로직에서 인코딩, 디코딩에서 사용해서 편리하게 처리하는 것으로 추측된다.

(장황하게 말했지만 당연한 말이다.)

@Serializable을 제거하고 빌드해봤다

 

 

 

3. Graph에 Destination 연결하기

Destination에는 3가지 일반적인 타입이 있다.

  • Hosted : NavHost 전체를 채우고, 이전 Desitnation이 보이지 않도록 NavHost 전체를 채움 (Screen)
  • Dialog : 오버레이 UI로 화면 사이즈에 얽매이지 않고, 밑에 깔린 이전 Destination이 보임
  • Activity 

 

Hosted 만들기

이것도 이전 글에 포스팅이 되어있는 사실!

 

[Android/Compose] Navigation 기억하기

왜인지 모르겠지만 Navigation은 항상 헷갈리는 토픽 중 하나였다이번 포스팅으로 Navigation의 구조이 대해서 좀 더 이해 + 기억할 수 있으면 좋겠다  Navigation의 주요 구성    NavHost (현재 Destination

junyoung-developer.tistory.com

composable<Destination> { DestinationScreen( /* ... */ ) }
  • composable 함수를 사용해서 Graph 안에 스크린을 정의해서 연결할 수 있다.

 

Dialog 만들기?!

dialog 또한 Navigation으로 연결할 수 있다는 사실... 왜 저만 여태 몰랐나요?

여태 개발하면서 Dialog를 Navigation에 연결할 일이 없었던 것 같은데, 공식 문서를 다시 읽다 발견했다.

  • Hosted Destination 위에 해당 Destination에 있는 UI를 dialog window 형태로 나타냄
  • Dialog Destination은 FloatingWindow 인터페이스를 구현
NavHost(navController, startDestination = Destination1) {
    composable<Destination1> { ... }
    dialog<Dialog1> { DialogScreen() }  // link Dialog Destination
}

궁금해서 테스트를 해보니 설명대로 Column으로 감싼 Composable을 띄워도 일반 다이얼로그처럼 나왔다. 

fillMaxSize()로 테스트한 화면

 

 

근데 여태 사용할 일이 없었던 이유도 알았다.

나의 경우, 대부분 두번째 다이얼로그를 사용했기 때문!

  • dialog()로 사용하는 경우 : 자체 수명주기와 state가 필요한 앱 내 Screen과 같은 속성을 갖는 다이얼로그
    • Graph 내 다른 Destination과는 별개
  • Dialog()로 사용하는 경우 : 사용자의 <확인>을 위해 띄우는 다이얼로그처럼 단순한 다이얼로그
    • Graph 내 특정 Destinaiton과 연관 (그 안에서 띄울테니까!)

 

Destination 간 데이터 전달하기

아래는 위의 코드를 예전의 코드로 바꿔본 예시 코드이다.

위에서 문상훈 짤을 쓸 정도로 기뻤던 이유를 알 수 있는... 예시라고 할 수 있다 👏👏

// Previous
NavHost(...) {
    composable(
        route = "destination2/{note}",
        arguments = listOf(
            navArgument("note") {
                type = NavType.StringType
    ) { backStackEntry ->
        val note = backStackEntry.arguments?.getString("note")
        Destination2Screen(note)
    }
}

모든 것을 String 값으로 관리하고 전달하다보니 당연히 휴먼 에러가 많이 발생할 수 밖에 없는 구조였다.

(물론 이걸 Extension 함수와 Enum과 이것저것으로 관리를 해오긴 했지만...!)

 

 

아래가 코드가 업데이트된 버전의 코드다.

@Serializable
data class Destination2(val note: String)
data class Destination3(val test: String? = null) // optional argument

// Now
NavHost(
    navController = navController, 
    startDestination = Destination2("This is test destination")
) {
    composable<Destination2> { backStackEntry ->
        val dest2: Destination2 = backStackEntry.toRoute()
        Destination2Screen(note: dest2.note)
    }
}
  • route 관리가 용이해짐
  • navArgument를 정의할 필요가 없어짐
  • optional argument의 경우, nullable 필드로 default value와 함께 생성해야 함

 

Destination을 @Serializable로 관리하면서 toRoute() 함수의 역할이 커보였다.

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)
}
  • 제네릭 타입으로 받은 Destination의 argument 타입도 따로 지정해주지 않아도 알아서 매핑
  • 우리가 정의한 argument 값이 내장된 Destination으로 디코딩해서 return 

argument 전달의 편의성이 아주 높아졌다는 점이 이번 업데이트의 가장 큰 장점이라고 생각한다.

 

중첩 Graph

Graph 안에 Graph를 또 그릴 수 있다.

  • navigation() 함수를 이용해서 다른 화면처럼 그릴 수 있음
  • 해당 함수 안에서 composable(), dialog()를 호출할 수 있음
NavHost(...) {
   composable<Destination1> {
       Destination1Screen()
   }
   
   navigation<Graph2>(startDestination = Destination2) {
       composable<Destination2> {
           Destination2Screen()
       }
       ...
   }
   ...
}

→ 그래프가 너무 커지는 경우, Nested Graph를 이용해서 분리하여 관리할 수 있다

 

 

 

4. Destination 간 이동하기

저번 포스팅을 봤다면 내가 만든 이미지를 봤을 것이다.

우리는 이동할 때 조종간을 이용해서 이동해야 한다.

navController.navigate(route = Destination1)
navController.navigate(route = Destination2("Hello"))
navController.navigate(route = NestGraph)

이 부분에 대해서는 Back Stack 관련된 다음 포스팅에 더 자세히 다루도록 하겠다!

그리고 이 글을 읽고 개념이해가 되었다면 공식문서의 예시가 잘 정리되어 있으니 확인해보면 좋을 것 같다!

 


Reference 

반응형