이 글은 안드로이드 공식 문서와 냉동 코딩님의 안드로이드 강의를 보고 작성하였다.
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko
ViewModel 개요 | Android 개발자 | Android Developers
ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.
developer.android.com
ViewModel
" ViewModel 클래스는 비즈니스 로직 또는 화면 수준 상태 홀더입니다. UI에 상태를 노출하고 관련 비즈니스 로직을 캡슐화합니다. 주요 이점은 상태를 캐시하여 구성 변경에도 이를 유지한다는 것입니다. 즉, 활동 간에 이동하거나 구성 변경(예: 화면 회전 시)을 따를 때 UI가 데이터를 다시 가져올 필요가 없습니다. " - 안드로이드 공식 문서 왈-
공식 문서는 참 말을 어렵게 하는 것 같다... 코드로 예시를 보면서 살펴보자.
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()
}
}
}
위 코드는 초기값(counter)을 100으로 설정하고 버튼을 누를 때 마다 1씩 증가하도록 만든 코드이다. 5번 눌러 105를 만들었고 스마트폰을 회전 시켜 가로 모드로 만들었더니 100으로 초기화 된다.
이유가 뭘까? 아래의 그림을 살펴보자
스마트폰을 회전시킬 때 앱의 생명주기이다. 액티비티가 처음 실행되었을 때 onCreate()에 진입하면서 100으로 초기화 되었고 버튼을 누르면서 1을 증가시켜 105로 만들었다.
화면을 회전 시키게 되면 다시 onCreate()에 진입하면서 counter 값이 다시 초기화된 것이다.
그러면 우리는 증가된 counter 값이 다시 초기화되지 않게끔 생명주기에 관계없이 값을 가지고 있어야 한다. 우리는 onSavedInstanceState()로 bundle 형태로 값을 저장하였다. 하지만 bundle은 50k로 적은 용량을 저장해야 하고 우리는 생명주기에 관계 없이 데이터를 보존하기 위해 ViewModel을 사용하게 된다.
즉, ViewModel에서 생명 주기와 상관없이 데이터를 저장하고 가져올 수 있게 된다는 것이다.
ViewModel
ViewModel을 적용하여 코드를 작성해보자.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 뷰모델 적용
val myViewModel = ViewModelProvider(this).get(MyViewModel::class.java) // 싱글턴 패턴
myViewModel.counter = 100
binding.textView.text = myViewModel.counter.toString()
binding.button.setOnClickListener {
myViewModel.counter += 1
binding.textView.text = myViewModel.counter.toString()
}
}
}
MyViewModel.kt
class MyViewModel : ViewModel() {
var counter : Int = 0
}
- MyViewModel 클래스를 생성하고 counter 값을 가져오도록 코드를 수정했다.
- 하지만 역시 onCreate()에서 myViewModel.counter = 100 에서 보듯 초기화는 onCreate()에서 발생하므로 여전히 회전 시 숫자가 100으로 돌아오는 문제점이 남아있다.
- ViewModelProvider에서 값을 초기화하는 것은 불가능하므로 팩토리 패턴을 사용하여 문제를 해결해보겠다.
팩토리 패턴 적용
MyViewModelFactoy.kt
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")
}
}
- MyViewModelFactory를 클래스를 추가한다.
- 팩토리 패턴을 사용하는 이유는 ViewModelProvider 에서 counter 값을 초기화하지 못하기 때문이다.
- 팩토리 패턴에 대해서는 글 작성 후 링크 남기겠다.
MyViewModel.kt
class MyViewModel(_counter : Int) : ViewModel() {
var counter = _counter
}
- 객체 생성 시 counter 값을 매개변수로 받을 수 있게 수정하였다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val factory = MyViewModelFactory(100, this)
val myViewModel : MyViewModel by viewModels { factory }
binding.textView.text = myViewModel.counter.toString()
binding.button.setOnClickListener {
myViewModel.counter += 1
myViewModel.saveState()
binding.textView.text = myViewModel.counter.toString()
}
}
}
- 팩토리를 추가하면서 생명주기에 상관없이 counter 값을 유지할 수 있게 되었다!!!!
- 하지만 앱이 강제 종료가 된 경우 ViewModel에서 counter의 값이 유지되지 못하는 문제가 생긴다.
- SavedStateHandle 을 사용하여 문제를 해결해보자!
SavedStateHandle
MyViewModelFactoy.kt
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
}
throw IllegalArgumentException("Viewmodel class not found")
}
}
MyViewModel.kt
class MyViewModel(
_counter : Int,
private val savedStateHandle:SavedStateHandle
) : ViewModel() {
var counter = savedStateHandle.get<Int>(SAVED_STATE_KEY)?: _counter
fun saveState(){
savedStateHandle.set(SAVED_STATE_KEY, counter)
}
companion object{
private const val SAVED_STATE_KEY = "counter"
}
}
MainActivity.kt
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()
// }
// val myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// myViewModel.counter = 100
// binding.textView.text = myViewModel.counter.toString()
//
// binding.button.setOnClickListener{
// myViewModel.counter += 1
// binding.textView.text = myViewModel.counter.toString()
// }
val factory = MyViewModelFactory(100,this)
// val myViewModel = ViewModelProvider(this,factory).get(MyViewModel::class.java)
// 위임
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()
}
}
}
- 앱 강제 종료 시에도 값이 유지됨을 확인할 수 있다.
정리
- 생명주기에 따라 변수의 값이 바뀔 수 있다. 우리는 생명주기에 상관없이 일정한 변수값을 가지기 위해 ViewModel을 사용한다.
- ViewModelProvider에서 변수값을 초기화할 수 없기 때문에 팩토리 패턴을 사용한다.
- 앱 강제 종료 시에도 값을 유지하고 싶으면 SaveStateBundle을 활용한다.
'안드로이드' 카테고리의 다른 글
[안드로이드 디자인 패턴] Repository Pattern (저장소 패턴) (1) | 2023.10.14 |
---|---|
[안드로이드 디자인 패턴] 싱글턴 패턴 (Singleton Pattern) (1) | 2023.10.08 |
[안드로이드 디자인 패턴] 안드로이드에서 observer pattern 적용 (setOnClickerListener) (0) | 2023.09.26 |