ViewModel은 Android Architecture Component중 하나로 android jetpack라이브러리 형태로 지원한다. 앞서 얘기했듯이, MVVM 모델의 ViewModel을 구현하는데 사용된다. Activity, Fragment들이 UI Controller로서 XML과 함께 화면 rotation의 경우처럼 UI가 destroy-create되는 상황에서도, ViewModel은 data를 들고 완전히 finished 되기 전까지 유지하게 된다.
ViewModel은 어쨌든 UI Activity나 Fragment에 대응하고 UI가 완전히 finished되면, 같이 destory되기 때문에 lifecycle의존도가 있고, lifecycle패키지 아래에 존재한다. 사용하려면, build.gradle 파일에 androidx의 다음 패키지를 추가한다. 버전이나 정확한 이름은 lifecycle의 릴리즈 노트부분을 참고.
android {
dependencies {
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
ViewModel 클래스는 다음과같이 상속받아 정의한다.
package com.example.android.guesstheword.screens.game
import android.util.Log
import androidx.lifecycle.ViewModel
class GameViewModel : ViewModel() {
init{
Log.i("GameViewModel", "GameViewModel created!")
}
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed")
}
}
onCleared() 메소드는 viewmodel이 더이상 필요하지 않아 제거될 때 호출된다.
Fragment사용시, 각 Fragment당 하나의 viewmodel이 필요하다. Fragment에서 viewmodel을 생성하면, destroy시 같이 없어지므로 ViewModelProvider를 이용해 인스턴스를 생성한다.
class GameFragment : Fragment() {
private lateinit var viewModel: GameViewModel
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
ViewModelProvider는 ViewModel이 이미 존재하면, 그 인스턴스를 돌려주고, 없으면 새로생성한다. ViewModelProvider에 인자로 this가 넘어가고 있는데, API문서를 보면, ViewModelStoreOwner 로 생성하는 ViewModel의 소유자를 지칭한다. 소유자의 lifecycle이 완전히 종료될 때, ViewModel의 onCleared()가 불리며 ViewModel의 인스턴스도 같이 소멸된다.
get()의 인자로는 GameViewModel의 클래스를 전달하고 있다.
당연하게도 viewmodel에 UI 요소의 레퍼런스가 존재하면 안된다. Fragment는 일시적으로 destroy될 수 있고, 이렇게되면 유효하지 않은 레퍼런스를 들고있게된다.
default ViewModel은 생성자에 인자가 없다. Fragment 사이에 값을 주고받는경우, ViewModel생성시에도 값을 전달해야 하는데, 인자가 추가된 생성자가 필요한 경우를 위해 ViewModelProvider에서 ViewModel Factory를 제공한다. ViewModelProvider.Factory를 상속받아 create() 함수를 override해서 Custom Factory를 만들고, ViewModel을 가져올 때, Factory도 인자로 넘겨주게된다.
package com.example.android.guesstheword.screens.score
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ScoreViewModelFactory(private val finalScore: Int): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(ScoreViewModel::class.java)){
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class ScoreFragment : Fragment() {
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score)
viewModel = ViewModelProvider(this, viewModelFactory).get(ScoreViewModel::class.java)
ScoreFragment에서 ViewModelFactory 인스턴스를 만들고, viewModel을 ViewModelProvider에서 가져올 때 인자로 같이 전달하고 있다.
ScoreViewModelFactory에는 Bundle로부터 arguments를 받아와 생성자에 넣어주고 있다. 이 Factory를 통해 인자가 있는 ViewModel을 얻어오게 된다. 이 작업은 ViewModelProvider에서 이루어지며, 생성한 Factory를 인자로 넘겨준다.
Live Data
ViewModel이 가지고 있는 data들중에 data binding으로 UI와 엮이는 값들은 LiveData로 들고 있게된다. LiveData는 Observable data holder이다. 이말은, Observer가 이 값에 연결되어 값이 변경될 때마다 알림을 받을 수 있다는 것이다. 단순한 Observable value와 다른점은 LifecycleOwner에 따라, UI의 유효한 라이프 사이클동안 작동한다.
그럼 사용법을 알아보자. 먼저 라이브러리를 추가해야한다.
dependencies{
def lifecycle_version = "2.2.0"
...
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
조금 특이한 점으로, 다음과같이 appcompat 라이브러리만 있어도 빌드에 문제가 생기지는 않는다.
implementation 'androidx.appcompat:appcompat:1.2.0'
이유는 정확히 모르겠지만, appcompat이 이전 버전 호환성을 위한 패키지이고 아마도 호환성을 위해 그렇게 되어 있는게 아닐까 추측된다.
Android Tutorial의 Guess The Word 프로젝트 예제를 살펴보자. 먼저, ViewModel에 변수 대신, MutableLiveData<T> 로 LiveData 인스턴스를 생성한다.
var word = MutableLiveData<String>()
var score = MutableLiveData<Int>()
값은 value property로 접근 할 수 있다.
init{
word.value = ""
score.value = 0
...
}
score.value = (score.value)?.plus(1)
...
word.value = wordList.removeAt(0)
값을 사용자 쓰레드에서 변경하는 경우, postValue()를 이용할 수 있다.
UI에서는 다음과 같이 Observer를 등록하면 따로 값 변경시 직접 설정해주지 않아도 자동으로 같이 변경된다. Observer의 등록은 LiveData.observe() 함수를 이용한다.
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
특이사항으로는 LifecycleOwner를 넘겨줘야 한다. 앞에서 얘기했듯이, UI가 유효한 lifecycle state(STARTED or RESUMED)에서만 작동하기 위함이다. Observer는 편의를 위해 lambda 표현식을 넘기고 있다. anonymous function으로 생각하면 된다.
LiveData값을 backing property를 이용해 encapsulation 시킬수도 있다. score를 다음과 같이 바꿔보자.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
외부에서 참조는 score를 읽기전용으로 만들고, backing property로 _score는 변경이 가능하다. MutableLiveData 와 LiveData로 인스턴스가 생성된 것도 알 수 있다. 이렇게 변경하면, 내부적으로 변경하는 값은 _score로 수정해줘야 한다.
init{
_score.value = 0
...
}
_score.value = (score.value)?.plus(1)
지금까지 설명한 내용을 다이어그램으로 그리면 다음과 같다.

Data Binding with LiveData
앞에서는 Fragment 코드에서 LiveData.observe()를 이용해 값을 모니터링 했었다. 그런데, data binding을 이용하면 이 과정없이 XML을 직접 연결할 수 있다. 이전에 data binding을 사용했었지만, LiveData를 사용하지 않으면, 값이 변경되도 연결된 UI에는 반영되지 않았다. 하지만, LiveData를 사용하면, 값의 변경이 바로 반영되게 된다.
예제프로젝트에서, score_fragment.xml 과 ScoreFragment 클래스를 보자. 먼저 score_fragment.xml에 data binding을위해 다음을 추가한다.
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
화면에 보여질 TextView의 text 부분을 추가한 data항목의 변수를 이용하여 표시한다.
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
이제 ScoreFragment 클래스에서 다음과같이 생성했던 viewModel을 xml의 변수에 할당해준다.
...
viewModel = ViewModelProvider(this, viewModelFactory).get(ScoreViewModel::class.java)
binding.scoreViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
...
주의할 점은 binding.lifecycleOwner에 viewLivecycleOwner를 할당하는 부분이다. 이게 자동으로 이뤄지지 않는게 조금 의아하지만, 직접 설정해줘야 하는 것으로 보인다.
이렇게하면, 앞서 사용했던 observe() 코드는 필요가 없어진다.
버튼클릭과 같은 이벤트 핸들러도 이를 이용해 바로 연결이 가능하다.
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
당연하게도 scoreViewModel에는 onPlayAgain()이 구현되어 있어야 한다.
data binding을 이용하여, 보다 간단해진 다이어그램을 얻어낼 수 있다.

이로서, MVVM 패턴에 View-ViewModel 부분이 완성되었다. ViewModel-Model 부분만 구현이 된다면, 전체 앱 아키텍쳐의 완성된 그림을 얻을 수 있다.