Compose Navigation에서 좀 더 간단하게 State를 초기화할 순 없을까?

Arakene·2025년 8월 15일

A화면에서 시작일과 종료일을 선택하고 그 선택값을 B화면에서 사용해야하는 상황에서 어떻게 하면 B화면의 State를 이쁘게 초기화할 수 있을까 고민하고 있었다.

As-is

원래는 파라미터로 받아서 LaunchedEffect로 초기화하거나 navHost에서 composable내부에서 toRoute()로 가져와 뷰모델에 넣고 뷰모델 자체를 인자로 넘기는 방식을 생각했었다.

composable<Screens.QuoteList> {

    val data = it.toRoute<Screens.QuoteList>()

    data.startDate

    WithBaseErrorHandling<ListViewModel>(logoutEvent = updatedLogoutEvent) {
        QuoteListView(
            navigate = {
                navController.navigate(it)
            },
            popBackStack = {
                navController.navigate(Screens.Home()) {
                    popUpTo(0) { inclusive = true }
                }
            },
            navController = navController
        )
    }
}

이런 식을 생각했었는데 아무리 생각해도 더 좋은 방법, 더 이쁜 초기화 방법이 있을 것 같아 찾아보기로 했다.

To-be

힐트를 사용해서 개발을 하다보면 한번쯤은 뷰모델에서 SavedStateHandle를 주입해서 사용해본적이 있을텐데 이 객체를 활용해서 따로 외부에서 설정하는 방법없이 해결할 수 있었다.

@Serializable
data class QuoteList(
	val startDate: String = ""
) 

이 데이터 클래스로 이동할 화면에 대해서 데이터를 가지고 있을 때 별다른 설정없이 viewModel: ListViewModel = hiltViewModel()을 사용하면 저 startDate에 접근할 수 있다.

@HiltViewModel
class ListViewModel @Inject constructor(
	...
    private val savedStateHandle: SavedStateHandle
) : BaseViewModel() {

    val test = savedStateHandle.getStateFlow(
        "startDate",
        ""
    )

어떻게 이런 마법같은 일이 되는걸까?

NavHost에서 화면을 지정하게 되면 NavBackStackEntry가 생성된다. 이 엔트리는 자체로 LifeCycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner의 역할을 한다.

public expect class NavBackStackEntry :
    LifecycleOwner,
    ViewModelStoreOwner,
    HasDefaultViewModelProviderFactory,
    SavedStateRegistryOwner {

    internal val context: NavContext?
    internal val immutableArgs: SavedState?
    internal var hostLifecycleState: Lifecycle.State
    internal val viewModelStoreProvider: NavViewModelStoreProvider?
    internal val savedState: SavedState?
	...
        /**
     * The arguments used for this entry. Note that the arguments of a NavBackStackEntry are
     * immutable and defined when you `navigate()` to the destination - changes you make to this
     * SavedState will not be reflected in future calls to this property.
     *
     * @return The arguments used when this entry was created
     */
    public val arguments: SavedState?

    /** The [SavedStateHandle] for this entry. */
    @get:MainThread public val savedStateHandle: SavedStateHandle
    ...

navController.navigate()를 호출한 시점에서의 데이터를 arguments를 통해 관리하고 이후에 데이터가 필요할때 역직렬화하거나 StaedState를 넘기게 된다.
composable 내부에서 데이터에 접근하는 route<T>()함수의 내부를 보면 이 arguments를 역직렬화해서 데이터 클래스를 제공하는걸 확인할 수 있다.


/**
 * Returns route as an object of type [T]
 *
 * Extrapolates arguments from [NavBackStackEntry.arguments] and recreates object [T]
 *
 * @param [T] the entry's [NavDestination.route] as a [KClass]
 * @return A new instance of this entry's [NavDestination.route] as an object of type [T]
 */
public inline fun <reified T> NavBackStackEntry.toRoute(): T = toRoute(T::class)

/**
 * Returns route as an object of type [T]
 *
 * Extrapolates arguments from [NavBackStackEntry.arguments] and recreates object [T]
 *
 * @param [route] the entry's [NavDestination.route] as a [KClass]
 * @return A new instance of this entry's [NavDestination.route] as an object of type [T]
 */
@OptIn(InternalSerializationApi::class)
@Suppress("UNCHECKED_CAST")
public fun <T> NavBackStackEntry.toRoute(route: KClass<*>): T {
    val savedState = arguments ?: savedState()
    val typeMap = destination.arguments.mapValues { it.value.type }
    return route.serializer().decodeArguments(savedState, typeMap) as T
}

이후 hiltViewModel에서 사용할때도 해당 엔트리에서 savedStateHandle을 주입해서 사용하니 위의 To-be에서 한 것처럼 데이터에 접근할 수 있었던거였다.

profile
안녕하세요 삽질하는걸 좋아하는 4년차 안드로이드 개발자입니다.

0개의 댓글