한줄 정리 : class를 사용
object는 싱글톤이기 때문에, 앱 전체에서 인스턴스가 하나만 존재
수명 주기(Lifecycle) 관리 불가능
다중 화면(여러 ViewModel 인스턴스) 불가
의존성 주입(DI) 불편
Lifecycle-aware
화면별 상태 분리 가능
테스트 및 의존성 주입 용이
구조적 권장사항
핵심은 ViewModelProvider와 remember 계열 함수를 잘 쓰는 것
ViewModel은 class MyViewModel(...) : ViewModel() 형태로 선언
Activity에서 ViewModelProvider로 생성 혹은 Compose 함수에서 viewModel()로 생성
필요한 의존성은 생성자에 직접 전달 (수동 DI)
// 1.1 모델 정의
class MyRepository {
fun getMessage() = "안녕하세요, Repository에서 가져온 데이터입니다."
}
// 1.2 ViewModel 정의
class MainViewModel(
private val repository: MyRepository
) : ViewModel() {
// Compose UI에서 관찰할 수 있는 상태
var message by mutableStateOf("")
private set
fun loadMessage() {
message = repository.getMessage()
}
}
// 2. Activity에서 ViewModel 생성 (Hilt 없이)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 의존성 수동 생성
val repository = MyRepository()
// ViewModelProvider로 ViewModel 생성
val mainViewModel = ViewModelProvider(
this,
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
}
)[MainViewModel::class.java]
setContent {
MainScreen(viewModel = mainViewModel)
}
}
}
// 3. Composable에서 ViewModel 사용
@Composable
fun MainScreen(viewModel: MainViewModel) {
// 상태 읽기
val message = viewModel.message
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = message)
Spacer(Modifier.height(16.dp))
Button(onClick = { viewModel.loadMessage() }) {
Text("메시지 불러오기")
}
}
}
한줄 정리 : 뷰모델에 의존성(매개변수) 있으면 팩토리, 없으면 그냥 viewmodel()
viewmodel()로도 간편하게 사용가능
class MainViewModel : ViewModel() {
var count by mutableStateOf(0)
private set
fun increment() { count++ }
}
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
Column {
Text("Count: ${viewModel.count}")
Button(onClick = { viewModel.increment() }) {
Text("증가")
}
}
}
뷰모델 팩토리가 반드시 필요함
class MainViewModel(private val repository: MyRepository) : ViewModel() {
val message = mutableStateOf("")
fun load() { message.value = repository.getMessage() }
}
class MainViewModelFactory(
private val repository: MyRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MainViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
@Composable
fun MainScreen() {
val repository = remember { MyRepository() }
val factory = remember { MainViewModelFactory(repository) }
val viewModel: MainViewModel = viewModel(factory = factory)
Column {
Text(viewModel.message.value)
Button(onClick = { viewModel.load() }) { Text("불러오기") }
}
}
한줄 정리 : 가급적이면 외부(상위)에서 매개변수로 넘겨주기
@Composable
fun MainScreen(viewModel: MainViewModel) {
// 사용
}
장점
UI 테스트 용이
→ 테스트 시 Mock ViewModel을 주입할 수 있습니다.
의존성 주입 명확
→ ViewModel 생성 로직이 한 곳(상위)에서만 이루어집니다.
재사용성 높음
→ 같은 Composable이 다른 ViewModel을 받아서 동작 가능.
단점
상위에서 반드시 ViewModel을 생성 후 전달해야 해서 코드가 조금 길어질 수 있음.
Navigation Graph나 Activity에서 생성 위치를 고려해야 함.
언제 쓰나
상태 관리의 주도권이 상위 계층에 있어야 할 때
(예: 여러 화면이 같은 ViewModel을 공유해야 하는 경우)
테스트/모듈화를 중시할 때
@Composable
fun MainScreen() {
val viewModel: MainViewModel = viewModel(factory = factory)
// 사용
}
장점
코드 간결, Composable 호출하는 곳이 단순해짐
상위에서 ViewModel 생성 로직을 몰라도 됨
단점
테스트 어려움
→ Composable 안에서 바로 생성하므로 Mock 주입이 힘듦
의존성 관리 분산
→ ViewModel 생성 로직이 여러 Composable에 흩어질 수 있음
재사용성 낮음
→ 해당 Composable은 특정 ViewModel에 강하게 결합됨
언제 쓰나
해당 화면만 쓰는 ViewModel이고, 테스트보다는 빠른 개발이 목적일 때
프로토타입, 개인 프로젝트, 화면-ViewModel가 1:1 고정 매칭일 때