안드로이드 시스템·Compose 용어 사전
상태viewModelScope · hiltViewModel

ViewModel 상태 홀더

화면보다 오래 사는 상태 보유자. 구성 변경을 넘고 Context를 몰라야 한다.

화면보다 오래 사는 상태 보유자.

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

    fun refresh() = viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true) }
        val users = repository.getUsers()
        _uiState.update { HomeUiState(users = users) }
    }
}

어떻게 얻나

by viewModels()                    // 이 Activity/Fragment 범위
by activityViewModels()            // 호스트 Activity 범위 (Fragment 끼리 공유)
hiltViewModel()                    // Compose + Hilt
viewModel()                        // Compose (기본 팩토리)

Compose 에서 hiltViewModel() 은 가장 가까운 ViewModelStoreOwner 에 묶인다

  • NavHost 안이면 그 목적지(백스택 엔트리) 범위다
  • 화면을 떠나면 ViewModel 도 정리된다 (원하는 동작)

화면 여러 개가 공유해야 하면 상위 그래프 엔트리에 묶는다 val parent = remember(entry) { navController.getBackStackEntry("flow") } val vm: FlowViewModel = hiltViewModel(parent)

viewModelScope

SupervisorJob + Dispatchers.Main.immediate 로 만들어진다

  • onCleared() 에서 자동 취소된다
  • 자식 하나가 실패해도 형제가 죽지 않는다
  • Main.immediate 라 이미 메인 스레드면 디스패치 없이 즉시 실행된다

Context를 절대 참조하지 않는다

// ❌ Activity 가 파괴돼도 ViewModel 이 붙들어 누수
class MyViewModel(private val activity: Activity) : ViewModel()

// ❌ View 참조도 같다
class MyViewModel : ViewModel() { var textView: TextView? = null }

// ✅ Context 가 필요한 일은 Repository 로 내린다

문자열 리소스도 마찬가지다 ViewModel 은 R.string.xxx (Int) 만 상태에 담고 실제 stringResource() 는 Composable 이 부른다

  • 언어 변경이 자동으로 반영되고 테스트도 쉬워진다

면접 함정

  • "ViewModel은 싱글턴"ViewModelStoreOwner마다 하나다. 화면이 다르면 다른 인스턴스다.
  • "onCleared에서 코루틴을 취소한다"viewModelScope를 썼다면 자동이다.

팩토리로 인자를 넘겨야 할 때

// Hilt 를 안 쓰거나 런타임 인자가 필요한 경우
class DetailViewModel(private val id: Long) : ViewModel() {
    companion object {
        fun factory(id: Long) = viewModelFactory {
            initializer { DetailViewModel(id) }
        }
    }
}
val vm: DetailViewModel = viewModel(factory = DetailViewModel.factory(itemId))

Hilt 를 쓴다면 대부분 SavedStateHandle 로 경로 인자를 받는 편이 낫다

  • 팩토리가 필요 없고 프로세스 죽음 복원도 함께 해결된다

onCleared에서 무엇을 하나

override fun onCleared() {
    super.onCleared()
    player.release()          // viewModelScope 가 아닌 자원
    connection.close()
}

코루틴은 자동 취소되므로 적을 것이 없다 직접 만든 자원(플레이어 · 소켓 · 리스너) 만 여기서 놓는다

주의: onCleared 는 프로세스가 강제 종료되면 안 불린다

상태 홀더가 항상 ViewModel일 필요는 없다

// UI 로직만 있는 상태 홀더는 그냥 클래스면 된다
class ScaffoldStateHolder(val listState: LazyListState, val scope: CoroutineScope) {
    fun scrollToTop() = scope.launch { listState.animateScrollToItem(0) }
}

@Composable
fun rememberScaffoldState(): ScaffoldStateHolder {
    val listState = rememberLazyListState()
    val scope = rememberCoroutineScope()
    return remember(listState, scope) { ScaffoldStateHolder(listState, scope) }
}

구분 UI 로직(스크롤·펼침·포커스) → 일반 상태 홀더 클래스

  • 비즈니스 로직·화면 상태 — → ViewModel

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — ViewModel·LiveData·StateFlow — 생명주기 안전 수집