JetPack Compose : ViewModel
MVVM 패턴으로 프로젝트를 개발하기로 해서 뷰모델은 꼭 알아야 할 것 같아서 오늘은 뷰모델을 공부했다.
뷰모델이 뭔지는 생략하고, 바로 실습 코드로 넘어가자.
class ContactsViewModel {
//ViewModel은 Compose 함수가 아니니까 remember 필요 없음
var backgroundColor by mutableStateOf(Color.White)
private set //외부에서 읽을 순 있지만 값을 바꿀 순 없도록
fun changeBackgroundColor() {
backgroundColor = Color.Red
}
}
우선 이런 식으로 뷰모델 클래스를 작성해 준다.
그 뒤 뷰모델을 사용할 액티비티에서
class MainActivity : ComponentActivity() {
private val viewModel = ContactsViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PhillipVMTheme {
Surface(
modifier = Modifier.fillMaxSize(),
//color = MaterialTheme.colorScheme.background
color = viewModel.backgroundColor
) {
Button(onClick = {
viewModel.changeBackgroundColor()
}) {
Text(text = "Change Color")
}
}
}
}
}
}
이런 식으로 뷰모델 인스턴스를 만들어 사용해 준다. 버튼을 누르면 배경색이 빨간색으로 바뀌게 된다.
하지만 이렇게만 하면 문제점이 있다. 실행시키고 버튼을 눌러 배경을 빨간색으로 바꾼 뒤 화면 회전시 다시 원래대로 돌아가게 된다는 것이다. 그 이유는 안드로이드에서 화면 회전이나 구성 변경(Configuration Change)이 발생하면, Activity가 완전히 새로 만들어지고 onCreate()가 다시 호출되기 때문이다. 이를 해결하기 위해선 위에서처럼 내가 임의로 정의만 한 뷰모델 클래스가 아닌, 안드로이드의 ViewModel클래스를 상속받아 사용해야 한다.
class ContactsViewModel: ViewModel()
private val viewModel by viewModels<ContactsViewModel>()
위의 코드를 이렇게 수정해 준다. 그러면 화면 회전이 발생해도 변한 색 그대로인 것을 확인할 수 있다.
더 개선해 보자. 우선 앱 수준 gradle에 다음을 추가해 준다.
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
그 후 다음과 같은 import문을 액티비티에 추가한다.
import androidx.lifecycle.viewmodel.compose.viewModel
그 뒤 메인액티비티를 다음과 같이 수정한다.
class MainActivity : ComponentActivity() {
//private val viewModel = ContactsViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PhillipVMTheme {
val viewModel = viewModel<ContactsViewModel>() //컴포즈 스타일
Surface(
modifier = Modifier.fillMaxSize(),
//color = MaterialTheme.colorScheme.background
color = viewModel.backgroundColor
) {
Button(onClick = {
viewModel.changeBackgroundColor()
}) {
Text(text = "Change Color")
}
}
}
}
}
}
흐음....실행 결과로만 보면 두 방식이 차이가 없어 보인다. 하지만 이 두 방식은 ViewModel을 어디서, 어떻게 관리하느냐에 차이가 있다.
| Activity delegate 방식 | Compose 스타일 | |
| 호출 장소 | Activity(또는 Fragment) 클래스 안 | Composable 함수 내부 |
| 라이프사이클 소유자 | Activity 자체 | 기본적으로 이 Composable을 포함하는 ViewModelStoreOwner (보통 Activity) |
| 주요 특징 | Activity가 재생성(예: 화면 회전)돼도 동일한 ViewModel 인스턴스 유지 Compose 내부든 외부든 같은 ViewModel 사용 가능 Compose 라이브러리 불필요 |
Composable 내부에서 바로 ViewModel 접근 가능 여러 Composable에서 독립적으로 ViewModel 관리 가능 화면 회전이나 Activity 재생성 시 Activity가 ViewModelStoreOwner라면 동일 ViewModel 유지, Fragment나 다른 Owner이면 Owner에 따라 새 인스턴스 생성 가능 별도의 라이브러리 필요 |
| 장점 | 안전하고 단순, Activity 재생성에 강함 | Compose 내부에서 자연스럽게 상태 바인딩 |
| 단점 | Compose 화면 여러 개에서 같은 Activity에 종속적임 | 라이브러리 필요 Activity-독립적이면 의도치 않게 새 ViewModel 생성 가능 |
이런 식으로 정리할 수 있다.
이제 뷰모델에 파라미터를 넣는 방법을 알아보자.
class ContactsViewModel(
val helloWorld: String
): ViewModel()
이런 식으로 파라미터를 추가해준 뒤,
class MainActivity : ComponentActivity() {
//private val viewModel = ContactsViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PhillipVMTheme {
val viewModel = viewModel<ContactsViewModel>(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContactsViewModel(
helloWorld = "Hello World"
) as T
}
}
) //컴포즈 스타일
Surface(
modifier = Modifier.fillMaxSize(),
//color = MaterialTheme.colorScheme.background
color = viewModel.backgroundColor
) {
Button(onClick = {
viewModel.changeBackgroundColor()
}) {
Text(text = "Change Color")
}
}
}
}
}
}
메인액티비티를 이렇게 수정해 준다.
원래 viewModel()은 파라미터 없는 기본 생성자 ViewModel만 생성 가능하다. 하지만 방금 ContactsViewModel 클래스에 파라미터를 추가했기 때문에, 다른 방법을 써야 한다. 이때 쓰는 것이 Factory이다.
Factory는 ViewModel을 어떻게 생성할지 알려주는 규칙을 정의하는 객체이다. 하나씩 파헤쳐 보자.
- create()
- modelClass: Class<T> → 어떤 ViewModel을 만들어야 하는지 알려줌
- T는 ViewModel 타입
- 반드시 ViewModel을 리턴해야 함
- as T → 제네릭 타입 캐스팅 (ViewModelProvider 요구 사항)
- 여러 ViewModel을 한 Factory에서 처리할 수도 있음
갑자기 뷰모델에 파라미터를 넣는 게 뜬금없을 수도 있다. 하지만 이는 매우 중요하다.
가장 큰 이유는 바로 외부 의존성을 주입하기 위함이다. ViewModel은 UI 상태와 로직을 관리하는 역할만 해야 한다(MVVM의 원칙) 만약 ViewModel이 Repository, UseCase 같은 외부 객체를 사용해야 하면, 파라미터로 받아서 주입하는 게 가장 깔끔하다.
즉, Activity나 Composable에서 직접 데이터를 넣지 않아도 되고, ViewModel이 독립적으로 동작 가능하도록 할 수 있다.
또한 테스트 시에도 파라미터만 바꿔서 쉽게 단위 테스트가 가능하다는 장점도 있다.
오전부터 뷰모델을 공부했으니 이제 좀 집가서 낮잠 자고 다시 할 거 해야겠다.