이 게시글은 Type-Safe Compose Navigation을 사용하고 있다는 것을 전제합니다. 만약 아직 Migration을 하지 않으셨다면 아래의 게시글을 통해 진행해 보세요!
Compose Navigation을 사용하면서 화면 이동간 데이터를 전달해야 하는 경우가 발생합니다. Type-Safe를 지원하는 Navigation을 사용하고 계시다면, 아래와 같이 사용하셨을겁니다.
composable<Route.Detail> { navBackStackEntry ->
val id = navBackStackEntry.toRoute<Route.Detail>().id
DetailScreen(id)
}
이 방법은 Navigation에서 데이터를 전달하는 매우 좋은 방법이지만, ViewModel에서 `id`에 해당하는 데이터를 얻기 위해서 아래와 같이 호출해야하는 상황이 생깁니다.
LaunchedEffect(Unit) {
viewModel.init(id)
}
이 방법도 나쁘지만은 않습니다. 하지만 `LaunchedEffect` 내에서 데이터를 초기화하면 화면이 컴포지션될 떄마다 다시 실행될 위험이 있고 컴포저블 함수의 라이프사이클이 ViewModel의 라이프 사이클과 다를 경우, 예기치 않은 동작을 초래할 수 있으며, 예상된 데이터 흐름이 방해받을 수 있습니다.
그렇다면
제가 가장 좋아하는 초기화 방법은 `WhileSubscribed`와 `collectAsStateWithLifecycle()`을 같이 사용하여 데이터를 초기화하는 방법입니다.
val countState: StateFlow<Int> = repository.getCount()
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = 0
)
WhileSubscribed로 StateFlow를 정의하면 구독이 활성화된 동안에만 데이터를 방출합니다. 즉, UI가 Flow를 수집하는 동안에만 데이터가 방출되고, UI가 수집을 중단하면 데이터 방출이 일시적으로 멈추게 됩니다.
여기에 collectAsStateWithLifecycle를 사용하면 안드로이드의 라이프사이클과 다른 컴포저블 함수에서 안드로이드 라이프 사이클에 맞게 데이터를 수집할 수 있게 합니다.
이 2개의 방법을 같이 사용하면 안드로이드 라이프사이클에 맞춰서 데이터를 수집할 수 있습니다.
SavedStateHandle
하지만 제가 좋아하는 방법은 ViewModel에 id를 메서드를 통해 전달해야 할 경우에는 사용하기 어렵습니다. 위의 방법은 ViewModel이 생성될 때 정의되기 때문입니다.
이 때, 이런 고민을 해결해 준것이 바로 `SavedStateHandle`입니다.
NavBackStackEntry는 사실 내부에 SavedStateHandle을 가지고 있습니다.
public class NavBackStackEntry
...
/** The [SavedStateHandle] for this entry. */
@get:MainThread
public val savedStateHandle: SavedStateHandle by lazy {
check(savedStateRegistryAttached) {
"You cannot access the NavBackStackEntry's SavedStateHandle until it is added to " +
"the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry " +
"reaches the CREATED state)."
}
check(lifecycle.currentState != Lifecycle.State.DESTROYED) {
"You cannot access the NavBackStackEntry's SavedStateHandle after the " +
"NavBackStackEntry is destroyed."
}
ViewModelProvider(this, NavResultSavedStateFactory(this))
.get(SavedStateViewModel::class.java)
.handle
}
..
NavBackStackEntry는 이는 NavController가 새로운 화면으로 이동을 할 때 생성됩니다. 이 과정에서 NavBackStackEntry는 내부에 SavedStateHandle을 생성합니다. 이 때, 네비게이션 경로로 전달된 모든 인자들은 이 SavedStateHandle에 저장됩니다.
그렇기 때문에 네비게이션간 보내지는 인자를 SavedStateHandle를 통해 얻을 수 있습니다.
ViewModel과 SavedStateHandle
ViewModel을 컴포저블에서 사용하기 위해 주입할 때, 일반적으로 Hilt가 널리 사용됩니다.
ViewModel을 Hilt로 주입하기 위해서는 `@HiltViewModel` 어노테이션을 통해 정의하여야 합니다. HiltViewModel은 기본적으로 SavedStateHandle의 생성자 주입을 지원하는데, 이는 내부 ViewModel 컴포넌트 빌더와 HiltViewModelFactory에서 지정된 대로 SavedStateHandle과 ViewModelLifecycle이라는 두 가지 기본 바인딩을 제공하기 때문입니다.
그렇기 때문에 매우 간단하게 SavedStateHandle을 생성자로 주입받을 수 있습니다.
@HiltViewModel
class DetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel()
이를 사용하는 것은 더 간단합니다. Type-Safe한 인자를 사용하기 위해서 필요했던 `toRoute<>` 메서드를 바로 SavedStateHandle에서도 사용할 수 있습니다.
public inline fun <reified T : Any> SavedStateHandle.toRoute(
typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap()
): T = internalToRoute(T::class, typeMap)
@OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun <T : Any> SavedStateHandle.internalToRoute(
route: KClass<T>,
typeMap: Map<KType, NavType<*>>
): T {
val map: MutableMap<String, NavType<*>> = mutableMapOf()
val serializer = route.serializer()
serializer.generateNavArguments(typeMap).onEach { map[it.name] = it.argument.type }
return serializer.decodeArguments(this, map)
}
이를 사용하는건 이제 매우 쉽습니다.
@HiltViewModel
class DetailViewModel @Inject constructor(
getUsecase: GetUsecase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val id = savedStateHandle.toRoute<Route.Detail>().id
val detailUiState: StateFlow<DetailUiState> = getUsecase(
id = id
).map {
DetailUiState.DetailInfo(it)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DetailUiState.Loading
)
}
마무리하며
이렇게 해서 매우 간단하게 SavedStateHandle를 통해 네비게이션간 전달되는 인자로 데이터를 초기화하는 방법을 알아보았습니다.
'Android > Compose' 카테고리의 다른 글
ViewModel 톺아보기 (1) - ViewModel의 생성과 관리 (0) | 2024.11.18 |
---|---|
Type Safety를 지원하는 Compose Navigation으로 이전하기 (1) | 2024.07.09 |
Compose의 WindowInsets (0) | 2024.04.23 |
Compose의 SnapShot 시스템 (0) | 2024.03.29 |
Compose의 Remember, RememberSaverable 정복하기 (1) | 2024.03.06 |