훌륭한 개발자가 되기 위하여
Jetpack Compose로 ViewModel 공유하기 본문
Jetpack Compose에서 ViewModel
Compose에서는 Composable 자체가 ViewModelStoreOwner가 아니므로 ViewModel의 범위를 Composable로 지정할 수 없고 ViewModel의 생명주기를 맞출 수 없다.
왜냐하면, ViewModelStoreOwner은 ComponentActivity, Fragment, NavBackStackEntry 만의 SubClass이기 때문이다.
ViewModelStoreOwner란?
ViewModelStoreOwner는 ViewModel을 저장하고 관리하는 컨테이너 역할을 하는 객체이다.
ViewModelStoreOwner가 configuration change로 인해 삭제되고 재생성되었을때 ViewModelStore의 이전 owner를 알수 있어야 한다.
만약 재생성이 안되는 경우, ViewModelStore에 clear 매서드를 호출하여 ViewModel이 더 이상 필요하지 않다고 알려야한다.(메모리에서 해제)
Navigation에서 ViewModelStoreOwner
Navigation Component에서 각 Navigation Destination은 NavBackStackEntry로 관리됨
NavBackStackEntry마다 ViewModelStoreOwner를 가지므로 ViewModel의 생명주기를 독립적으로 관리할 수 있다.
Compose의 viewModel()
1. LocalViewModelStoreOwner.current를 조회하여 현재 Composable이 속해 있는 ViewModelStoreOwner(보통 NavBackStackEntry)를 가져온다.
2. ViewModelStoreOwner.get()을 호울하여 해당 BackStackEntry가 보유한 ViewModelStore에서 ViewModel을 조회하거나 없으면 Factory를 통해 새로 생성한다.
- Key : 동일 타입 ViewModel을 여러 개 만들어야 할 때 구분자 역할
- Factory : HiltViewModelFactory / DefaultFactory 등 실제 생성 책임
- Extras : SavedStateHandle, default arguments 등 추가 정보 포함
@Composable
public fun <VM : ViewModel> viewModel(
modelClass: KClass<VM>,
viewModelStoreOwner: ViewModelStoreOwner =
checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras =
if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
viewModelStoreOwner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
): VM = viewModelStoreOwner.get(modelClass, key, factory, extras)
SharedViewModel 적용 전
Select화면에서 선택한 운동 정보를 Safe Args를 이용해 전달하여 Record화면에서 출력하는 형태
Select 화면
@Composable
fun SelectScreen (
onExerciseRecordClick: (String, List<ExerciseModel>) -> Unit,
viewModel: SelectViewModel = hiltViewModel()
) {
// ...
}
@HiltViewModel
class SelectViewModel @Inject constructor(
// ...
): ViewModel() {
// ...
}
Record 화면
@Composable
fun RecordScreen (
onBackClick: () -> Unit,
viewModel: RecordViewModel = hiltViewModel()
) {
// ...
}
@HiltViewModel
class RecordViewModel @Inject constructor(
// ...
): ViewModel() {
// ...
}
Navigation 설정
composable<CalendarRoute.Select> {
SelectExerciseRoute(
onBackClick = onBackClick,
onExerciseRecordClick = { date, exerciseIdList ->
onExerciseRecordClick(
date,
exerciseIdList
)
}
)
}
composable<CalendarRoute.Record>(
typeMap = mapOf(typeOf<List<ExerciseNavArg>>() to ExerciseNavArgListType)
)
{
ExerciseRecordRoute(
...
)
}
※ 기능 추가로 인해 발생한 문제
`운동 추가하기` 기능을 도입하면서 다음 시나리오가 발생한다.
Record -> Select -> Record
- 사용자는 Record 화면에서 새로운 운동을 추가하기 위해 Select 화면으로 이동
- Select 화면에서 추가할 운동 선택
- 다시 Record로 돌아옴
- 기존 운동 리스트 + 새로 추가한 운동 리스트가 합쳐져야 함
이 때, 각각 독립적인 SelectViewModel / RecordViewModel 인스턴스가 생성됨
RecordViewModel의 init 블록이 다시 실행되면서 기존 운동 리스트가 날아감
👉 화면 전환 시에도 기존 운동 정보를 유지해야하기에 이때 필요한 것이 공유하는 ViewModel [SharedViewModel]이다.
Compose Navigation에서 ViewModel 공유하기
- 상위 NavBackStackEntry를 이용해 여러 Composable 목적지에서 뷰모델을 공유할 수 있음
- 공유할 ViewModel은 상위 NavBackStackEntry가 아직 백스택에 있다면, 계속 메모리에 살아있기 때문이다.
- Jetpack Compose에서는 NavBackStackEntry를 활용해 네비게이션 스택이 ViewModel의 상태를 인지하도록 설정하여 불필요한 재생성을 방지할 수 있다.
- 상위 NavBackStackEntry 를 구하고, 그 NavBackStackEntry 의 뷰모델을 가져와서 각 컴포저블 목적지에 전달한다
@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)
}
composable<CalendarRoute.Select> { backStackEntry ->
val sharedViewModel = backStackEntry.sharedViewModel<CalendarSharedViewModel>(navController)
SelectExerciseRoute(
viewModel = sharedViewModel,
...
)
}
composable<CalendarRoute.Record> { backStackEntry ->
val sharedViewModel = backStackEntry.sharedViewModel<CalendarSharedViewModel>(navController)
ExerciseRecordRoute(
viewModel = sharedViewModel,
...
)
}
HiltViewModel을 사용하는 경우
- 상위 NavBackStackEntry에 접근한 뒤, hiltViewModel의 파라미터에 넣어주면 된다.
composable<CalendarRoute.Select> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(MainRoute.Calendar)
}
val sharedViewModel = hiltViewModel<CalendarSharedViewModel>(parentEntry)
SelectExerciseRoute(
viewModel = sharedViewModel,
...
)
}
composable<CalendarRoute.Record> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(MainRoute.Calendar)
}
val sharedViewModel = hiltViewModel<CalendarSharedViewModel>(parentEntry)
ExerciseRecordRoute(
viewModel = sharedViewModel,
...
)
}
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
참고 자료
https://developer.android.com/topic/libraries/architecture/viewmodel
https://medium.com/@mousaieparniyan/shared-viewmodel-in-jetpack-compose-1328f5c895c5
https://velog.io/@kej_ad/Android-Compsoe-Jetpack-Navigation-Nested-Graph%EC%99%80-Shared-ViewModel
'안드로이드' 카테고리의 다른 글
| 클린아키텍처에서의 예외 처리하기 (0) | 2025.11.28 |
|---|---|
| Jetpack Compose로 Custom Calendar 구현하기 (0) | 2025.10.26 |
| Paging 검색 기능에 Debounce를 적용하여 성능 개선하기 (0) | 2025.10.02 |
| Jetpack Compose에서 Paging3로 대용량 데이터 처리하기 (0) | 2025.09.18 |
| [JetpackCompose] Compose Side Effect (0) | 2025.08.14 |