본문 바로가기
Android

Type-Safety Navigation (1)

by Jiwon_Loopy 2025. 1. 27.
반응형

오늘은 Type-Safety 네비게이션에 대해 알아보겠습니다.

 

해당 내용은 안드로이드 Developers 공식 문서에서 발췌하였습니다.

 

Kotlin DSL 및 Navigation Compose의 유형 안전성  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin DSL 및 Navigation Compose의 유형 안전성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 내장된 유형

developer.android.com

 

1. Type-Safety Navigation?

 

Navigation 2.8.0 이상부터 도입되었으며, 기존의 Route 방식에서 벗어나 Custom Data Class값을 전달하고, 타입 불일치를 방지할 수 있다. 또한, 문자열을 추출하는 것이 더욱 편리해졌다.

 

 

2. 내가 사용한 방법

기존 네비게이션은 문자열로 지정된 route값과, backStackEntry를 통해 값을 가저오는 식으로 사용했었다.

나는 컴포즈를 처음 배우는 입장에서 아예 기존 방식이 아닌, 새로운 방식을 택하여 적용해 보는 것이기 때문에 틀렸을 수도 있지만... 사용한 방식을 기록해보려 한다.

해당 방식은 now in android 깃허브를 참조하여 만들어 보았다.

 

 

1. 데이터 클래스 선언

// 넘겨줄 데이터가 없는 경우
@Serializable
data object MakePlanetNavigation


// 넘겨줄 데이터가 있는 경우
@Serializable
data class PreviewRoute(
    val initialCardId: String = "",
    val initialPostType: PostType = PostType.NOT
)

 

넘겨줄 데이터를 포함한 데이터 클래스를 만들어 주었다. 값이 없는 경우는 data object로 처리하였다.

이 때 꼭 @Serializable 어노테이션을 통해 직렬화 해주어야 하고, Url 같은 경우는 상황에 맞추어 인코딩/디코딩 작업을 해주어야 한다. (외부 이미지의 주소 같은 것을 넘겨주어야 할 때)

 

 

 

2, NavHost에 등록해 줄 함수 정의

fun NavGraphBuilder.previewScreen(
    navController: NavController,
    planetViewModel: PlanetViewModel
) {
    composable<PreviewRoute>(
        deepLinks = listOf(
            navDeepLink {
                uriPattern = "$uri/preview"
            }
        )
    ) {
        PreviewScreen(
            navController = navController
        )
    }
}

 

코드를 보면 route가 없는 것을 알 수 있다. route 대신 내가 만든 data class를 넣어주었다.

그리고, 해당 네비게이션을 NavHost에 등록해 주었다.

 

 

 

3. 뷰 모델에서 가저와서 쓰기

@HiltViewModel
class PreviewViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository,
    private val userPrefRepository: UserPrefRepository
) : ViewModel() {

	val cId = savedStateHandle.toRoute<PreviewRoute>().initialCardId
	val pType = savedStateHandle.toRoute<PreviewRoute>().initialPostType
   
    val previewUiState: StateFlow<PreviewUiState> = previewUiState(
        topicId = cId,
        postType = pType
    ).stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = PreviewUiState.Loading,
    	)
        
        //...
    }

 

화면이 이동하는 순간 NavBackStackEntry가 생성되면서 자체적으로 내부에 SavedStateHandle을 생성한다. 이 안에는 우리가 만들어줬던 data class의 인자들이 저장이 된다. 이렇게 저장된 인자들은 viewModel에서 가저와서 사용할 수 있는데, toRoute를 이용하여 값을 가저올 수 있다. 

이것이 가능한 이유는 HiltViewModel이 자체적으로 ViewModel 내부에 SavedStateHandler를 주입시켜주기 때문이다.

 

각 라이프 사이클을 고려하여, StateIn과 WhileSubscribed를 활용하여 Flow를 수집할 경우에만 데이터를 방출하게 만들어 주었고, State를 관리해줄 interface를 만들어 연결시켜주었다.

 

 

 

4. 응용

private fun previewUiState(
        topicId: String,
        postType: PostType
    ): StateFlow<PreviewUiState> {
        return MutableStateFlow<PreviewUiState>(PreviewUiState.Loading).apply {
            Log.d("State",topicId + postType)
            if (topicId.isNotEmpty()) {
                viewModelScope.launch {
                    userRepository.getMainCard(topicId).collect { fetchedCard ->
                        value = if (fetchedCard != null) {
                            _peopleCount.value = fetchedCard.participatePeople
                            PreviewUiState.Success(
                                cid = topicId,
                                postType = postType,
                                userCard = fetchedCard
                            )
                        } else {
                            Log.e("PlanetViewModel", "Error: fetchedCard is null for topicId $topicId")
                            PreviewUiState.Error
                        }
                    }
                }
            } else {
                Log.e("PlanetViewModel", "Error: fetchedCard is null for topicId $topicId")
                value = PreviewUiState.Error
            }
        }
    }

sealed interface PreviewUiState {
    data class Success(
        val cid: String,
        val postType: PostType,
        val userCard: UserCard
    ) : PreviewUiState

    data object Error : PreviewUiState
    data object Loading : PreviewUiState
}

 

사용하는 것은 자유이지만, 나는 now in android의 방법을 적용해보기로 하였다.

각 상황에 맞추어 상태값들을 Sealed interface를 만든 뒤, StateFlow로 등록해준다.

그 후, 해당 UI Composable 에서 CollectAsState로 구독하여 상태에 따라 알맞은 화면을 뿌려주는 방식을 사용하였다.

 

아래는 참조한 now in android의 깃허브의 코드이다.

 

nowinandroid/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt at main · andro

A fully functional Android app built entirely with Kotlin and Jetpack Compose - android/nowinandroid

github.com

 

추가로 공식 문서의 설명이 많이 빈약하여 참고한 블로그가 있는데, 너무 소개가 잘 되어있어 공부하는 시간을 많이 줄일 수 있었다. 아래에 출처를 남겨놓겠다.

 

Type Safety를 지원하는 Compose Navigation으로 이전하기

사진: Unsplash의Dan Chung  드디어 Compose Navigation에서 Type Safety를 지원합니다! 이전에 Compose를 해보지 않으셨다면 모르시겠지만, 이전의 Compose Navigation을 해보셨다면 해당 방식이 마음에 안드셨던

everyday-develop-myself.tistory.com

 

SavedStateHandle을 통해 Compose Navigation간 데이터 전달하기

사진: Unsplash의Pawel Czerwinski 이 게시글은 Type-Safe Compose Navigation을 사용하고 있다는 것을 전제합니다. 만약 아직 Migration을 하지 않으셨다면 아래의 게시글을 통해 진행해 보세요! Type Safety를 지

everyday-develop-myself.tistory.com

 

728x90
반응형