ViewModel이란 Android Jepack의 구성 요소 중 하나로, 본래 ViewModel이란 이름은 소프트웨어 디자인 패턴 중 하나인 MVVM(Model - View - ViewModel) 디자인 패턴으로부터 파생되었다.
MVVM의 관점에서 봤을 때 ViewModel은 View로부터 독립적이며, View가 필요로 하는 데이터만을 소유한다.
안드로이드 앱 개발시에도 MVVM 디자인 패턴을 적용하면 Activity나 Fragment 같은 UI 컨트롤러의 과도한 책임을 분담하여 클래스가 거대해지는 것을 방지하고, 유지보수, 재사용성 그리고 테스트 등을 용이하게 만들어 준다.
ViewModel은 Activity에서는 Activity가 완전히 종료될 때까지, 그리고 Fragment에서는 Fragment가 분리될 때까지 메모리에 남아있도록 설계 되어있다.
위의 그림을 보면 Activity의 생명주기와 ViewModel의 생명주기를 함께 확인할 수 있다. 액티비티가 최초 생성될 때 일반적으로 ViewModel을 인스턴스화하여 생명주기를 함께 시작한다.
Configuration 변경(예: 화면 회전)이 발생할 때 액티비티가 다시 시작되는 것을 확인할 수 있다.
하지만 ViewModel은 여전히 메모리 상에 남아있는다.
이는 Activity 내부에서 Configuration 변경과 무관하게 유지 되는 NonConfigurationInstances 객체를 따로 관리하기 때문이다.
Activity의 finish() 호출 등에 의한 액티비티가 생명주기가 종료됨에 따라 내부의 LifecycleEventObserver를 통해 ViewModel도 onCleared() 콜백 메서드를 호출하고 종료된다.
버튼을 클릭했을 때 현재 값에서 1을 더해 값을 카운트 해주는 테스트 앱으로 화면을 가로로 회전 했을 때 초기값인 100으로 돌아가는 것을 확인할 수 있다.
// ViewModel을 사용하지 않은 코드
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
var counter = 100
binding.textView.text = counter.toString()
binding.button.setOnClickListener {
counter += 1
binding.textView.text = counter.toString()
}
}
}
ViewModel을 사용하여 ViewModel을 초기화할 때 초기값 100을 건네주고 나머지는 로직에서 저장 된 값을 사용하도록 할 경우 위의 영상과 같이 화면을 회전해도 값이 초기화 되지 않는 것을 확인할 수 있다.
// ViewModel 클래스를 상속하는 서브 클래스를 정의한다.
class MyViewModel(_counter: Int) : ViewModel() {
var counter = _counter
}
/*
ViewModel 객체를 만들 때 초기값을 전달하는 것이 금지되어 있기 때문에 Factory 패턴을 사용해야 됨
ViewModelProvider를 상속 받는 Factory 클래스
*/
class MyViewModelFactory(private val counter: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(counter) as T
}
throw IllegalArgumentException("Viewmodel class not found")
}
}
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// ViewModel에 전달하고 싶은 초기값을 Factory에 전달
val factory = MyViewModelFactory(100)
/*
ViewModel 인스턴스 생성
ViewModel 인스턴스를 그냥 생성하면 경우에 따라 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있음
ViewModelProvider를 통해 인스턴스를 싱글톤으로 생성하도록 함
*/
val myViewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)
binding.textView.text = myViewModel.counter.toString()
binding.button.setOnClickListener {
myViewModel.counter += 1
binding.textView.text = myViewModel.counter.toString()
}
}
}
ViewModel 객체는 ViewModelProvider를 사용해 생성할 수도 있지만
by
키워드를 이용해 간편하게 만들 수도 있다.
dependencies {
...
// viewModel 사용을 위한 dependency
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
implementation("androidx.activity:activity-ktx:1.8.2") // add
implementation("androidx.fragment:fragment-ktx:1.6.2") // add
}
by viewModels()
를 사용하면 ViewModelProvider를 사용하지 않고 ViewModel을 지연 생성할 수 있다. private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// ViewModel에 전달하고 싶은 초기값을 Factory에 전달
val factory = MyViewModelFactory(100)
// by viewModels 사용
val myViewModel by viewModels<MyViewModel>() {factory}
binding.textView.text = myViewModel.counter.toString()
binding.button.setOnClickListener {
myViewModel.counter += 1
binding.textView.text = myViewModel.counter.toString()
}
}
}
앱이 강제 종료 된 경우에도 View Model의 데이터를 저장하려면
Save State
핸들을 사용하면 된다.
Save State
는 많은 데이터를 저장할 수는 없지만 시스템에 의한 강제 종료가 발생해도 살아남을 수 있다.
ViewModel | 저장된 인스턴스 상태 | 영구 저장소 | |
---|---|---|---|
저장소 위치 | 메모리 내 | 디스크에 직렬화 | 디스크 또는 네트워크 내 |
구성 변경 시에도 유지 | 예 | 예 | 예 |
시스템에서 시작된 프로세스 중단 시에도 유지 | 아니요 | 예 | 예 |
사용자의 완전한 활동 닫기 /onFinish() 시에도 유지 | 아니요 | 아니요 | 예 |
데이터 제한 | 복잡한 객체도 괜찮지만 사용 가능한 메모리에 의해 공간이 제한 됨 | 원시 유형 및 문자열과 같은 단순하고 작은 객체만 해당 | 디스크 공간 또는 네트워크 리소스에서 검색하는 비용/시간에 의해서만 제한됨 |
읽기/쓰기 시간 | 빠름(메모리 액세스만) | 느림(직렬화/역직렬화 및 디스크 액세스 필요) | 느림(디스크 액세스 또는 네트워크 트랜잭션 필요) |
dependencies {
...
// viewModel 사용을 위한 dependency
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
// by viewModels()
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
// save state
implementation("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha11") // add
}
class MyViewModel(_counter: Int,
private val savedStateHandle: SavedStateHandle) : ViewModel() { // 생성자로 savedStateHandle을 받도록 함
// savedStateHandle에서 값을 가져오게 하는데 만약 null이면 전달 받은 초기값을 사용하도록 함
var counter = savedStateHandle.get<Int>(SAVE_STATE_KEY) ?: _counter
fun saveState() {
savedStateHandle.set(SAVE_STATE_KEY, counter) // count 값을 저장하는 SaveState를 정의
}
companion object {
// savedStateHandle은 Key Value 형태로 값을 저장하기 때문에 저장과 복원에 사용할 Key를 정해줌
private const val SAVE_STATE_KEY = "counter"
}
}
class MyViewModelFactory(
private val counter: Int,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(counter, handle) as T // ViewModel을 반환할 때 핸들을 함께 반환하도록 함
}
throw IllegalArgumentException("Viewmodel class not found")
}
}
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// ViewModel의 카운터 값을 save state에 연결해서 시스템에 의한 종료가 발생해도 값이 유지 되도록
val factory = MyViewModelFactory(100, this)
val myViewModel by viewModels<MyViewModel>() { factory }
binding.textView.text = myViewModel.counter.toString()
binding.button.setOnClickListener {
myViewModel.counter += 1
binding.textView.text = myViewModel.counter.toString()
myViewModel.saveState() // 카운터 값을 늘릴 때마다 그 값을 save state를 통해서 save state 핸들에 값을 저장
}
}
}