A화면에서 시작일과 종료일을 선택하고 그 선택값을 B화면에서 사용해야하는 상황에서 어떻게 하면 B화면의 State를 이쁘게 초기화할 수 있을까 고민하고 있었다.
원래는 파라미터로 받아서 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
)
}
}
이런 식을 생각했었는데 아무리 생각해도 더 좋은 방법, 더 이쁜 초기화 방법이 있을 것 같아 찾아보기로 했다.
힐트를 사용해서 개발을 하다보면 한번쯤은 뷰모델에서 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에서 한 것처럼 데이터에 접근할 수 있었던거였다.