[Android] NavBackStackEntry와 컴포즈에서의 ViewModel 공유

KSK·2025년 3월 16일

사전지식

  • Navigation 스택의 각 항목을 말함
  • Navigation 스택에 쌓인 각 화면이나 목적지에 대한 정보 제공 및 인스턴스 관리
  • 특정 네비게이션 경로의 정보인 NavDestination과 전달받은 arguments 정보를 갖고 있는 객체
  • 각 NavBackStackEntry 항목에는 ViewModelStore가 포함되어 있다.
  • SavedStateHandle을 통해 Configuration Change 발생 시 데이터 유지가 가능하고 전달도 가능
public class NavBackStackEntry
private constructor(
    private val context: Context?,
    /**
     * The destination associated with this entry
     *
     * @return The destination that is currently visible to users
     */
    @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public var destination: NavDestination,
    private val immutableArgs: Bundle? = null,
    private var hostLifecycleState: Lifecycle.State = Lifecycle.State.CREATED,
    private val viewModelStoreProvider: NavViewModelStoreProvider? = null,
    /**
     * The unique ID that serves as the identity of this entry
     *
     * @return the unique ID of this entry
     */
    public val id: String = UUID.randomUUID().toString(),
    private val savedState: Bundle? = null
) :
    LifecycleOwner,
    ViewModelStoreOwner,
    HasDefaultViewModelProviderFactory,
    SavedStateRegistryOwner

ViewModelStoreOwner

  • ViewModel을 저장하고 관리하는 객체
  • ViewModelProvider는 ViewModelStoreOwner를 통해 ViewModel을 관리하고 액세스
  • ComponentActivity, Fragment, NavBackStackEntry 의 SubClass
    Composable 함수는 ViewModelStoreOwner 가 아님!
  • Activity, Fragment, NavBackStackEntry 에서 관리되는 ViewModelStore는 해당 Scope가 파괴될때 clear() 된다.
    -> 컴포즈에선 해당 컴포저블의 NavBackStackEntry 가 내비게이션 스택에서 제거될때 clear
  • ViewModelStore : ViewModel을 Map 형태로 저장을 하는 객체
  • 내비게이션 컴포넌트에서, 각 내비게이션 Destination은 NavBackStackEntry 로 관리됨
  • NavBackStackEntry 마다 ViewModelStoreOwner를 가짐
  • 즉 백스택의 NavBackStackEntry 별로 뷰모델을 관리할 수 있으며, 뷰모델의 생명주기는 NavBackStackEntry에 연결된다 ← 핵심

Compose의 viewModel() 함수

  • NavBackStackEntry의 ViewModelStoreOwner를 통해 현재 BackStackEntry에 대한 ViewModel를 찾는다
  • LocalViewModelStoreOwner는 현재 BackStackEntry의 ViewModelStoreOwner를 제공
  • viewModel() 함수는 LocalViewModelStoreOwner에서 가져온 ViewModelStoreOwner에 접근, ViewModelProvider를 통해 필요한 ViewModel을 가져온다

Compose Navigation 에서 ViewModel 공유하기

  • 상위 NavBackStackEntry 이용해 여러 컴포저블 목적지에서 뷰모델을 공유할 수 있음
    • 공유할 뷰모델은 상위 NavBackStackEntry가 아직 백스택에 있다면, 계속 메모리에서 살아있기 때문
  • NavHost에서 navigation 함수로 뷰모델을 공유받을 Composable 목적지 끼리 묶음 (Nested Graph 구조)
NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        navigation(
            startDestination = MainScreen.Home.route,
            route = MainScreen.Main.route
        ) {
            composable(MainScreen.Home.route) {
                ..
            }
            composable(MainScreen.MyPage.route) {
                ..
            }
    }
}
  • 상위 NavBackStackEntry 를 구하고, 그 NavBackStackEntry 의 뷰모델을 가져와서 각 컴포저블 목적지에 전달한다
@Composable
NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        navigation(
            startDestination = MainScreen.Home.route,
            route = MainScreen.Main.route
        ) {
            composable(MainScreen.Home.route) {
                val viewModel = it.sharedViewModel<HomeViewModel>(navController)
                HomeScreen(viewModel)
            }
            composable(MainScreen.MyPage.route) {
                val viewModel = it.sharedViewModel<HomeViewModel>(navController)
                DetailScreen(viewModel)
            }
    }
}

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(navController: NavController): T {
    val navGraphRoute = destination.parent?.route ?: return viewModel()
    val parentEntry = remember(this) {
        navController.getBackStackEntry(navGraphRoute)
    }
    return viewModel(parentEntry)
}
  • Home과 MyPage 둘다 동일한 부모 NavBackStackEntry 인 Main의 HomeViewModel을 가져와서 사용할 수 있게됨

HiltViewModel을 사용하는 경우

  • 상위 NavBackStackEntry에 접근한 뒤, hiltViewModel()의 파라미터에 넣어주면 된다.
@Composable
NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        navigation(
            startDestination = MainScreen.Home.route,
            route = MainScreen.Main.route
        ) {
            composable(MainScreen.Home.route) {
                val parentEntry = remember(it) {
                    navController.getBackStackEntry(CreatePickDestinations.SEARCH_ROUTE)
                }
                val viewModel = hiltViewModel<HomeViewModel>(parentEntry)
                HomeScreen(viewModel)
            }
            composable(MainScreen.MyPage.route) {
		            val parentEntry = remember(it) {
                    navController.getBackStackEntry(CreatePickDestinations.SEARCH_ROUTE)
                }
                val viewModel = hiltViewModel<HomeViewModel>(parentEntry) 
                // parentEntry를 hiltViewModel 함수 파라미터에 사용
                DetailScreen(viewModel)
            }
    }
}

  • hiltViewModel()의 코드를 보면 파라미터로 viewModelStoreOwner를 받는다.
  • NavBackStackEntry는 viewModelStoreOwner를 상속받는 객체이므로 위와 같은 코드가 가능함
  • 즉 hiltViewModel에서 파라미터로 NavBackStackEntry과 같은 viewModelStoreOwner를 받는다면, 이 viewModelStoreOwner의 viewModelStore에서 뷰모델을 가져와서 사용한다는 의미

  • createHiltViewModelFactory 함수는 viewModelStoreOwner를 파라미터로 받는다.
  • viewModelStoreOwner가 NavBackStackEntry라면 HiltViewModelFactory를 만들어 리턴하는 것이다
  • 그리고 createHiltViewModelFactory 가 리턴한 HiltViewModelFactory에서 viewModel() 함수로 뷰모델을 리턴하는 것이 바로 hiltViewModel()의 동작이라 할 수 있다.

참고

https://velog.io/@kej_ad/Android-Compsoe-Jetpack-Navigation-Nested-Graph%EC%99%80-Shared-ViewModel#viewmodel

https://dudgus907.tistory.com/91

https://developer.android.com/reference/androidx/navigation/NavBackStackEntry

https://amuru.tistory.com/257

profile
그런게어딨어그냥하는거지

0개의 댓글