Android Jetpack 은 개발자가 고품질 앱을 쉽게 개발할 수 있도록 지원하는 라이브러리, 도구, 가이드의 모음 입니다. Jetpack 은 androidx.* 패키지 라이브러리로 지원되며, 크게 4가지 구성요소(기초, 아키텍처, 동작, UI)를 가집니다.

그 중 아키텍처 구성요소(AAC, Android Architecture Component)는 Data binding, Lifecycle, Live data, Room, Work manager 등을 포함하고 있습니다. 오늘 얘기할 주제인 ViewModel 도 여기에 포함됩니다.

https://developer.android.com/jetpack?hl=ko

물론 ViewModel 은 MVVM 패턴을 구현하는데 요긴하게 사용할 수 있지만, 근본적으로 Activity 와 같은 UI 관련 클래스의 수명 주기를 고려해서 UI 관련 데이터를 관리하는데 초점이 맞춰져 있습니다. 그래서 화면의 회전, 언어(Locale) 변경과 같은 activity의 상태 변화에도 UI 데이터의 일관성을 유지할 수 있도록 만들어져 있습니다.

화면의 회전처럼 activity가 재시작되는 상황에서 기존에 activity가 제어하던 UI 데이터가 덩달아 초기화 되지 않도록 처리하는 일은 흔히 발생하는 상황이며, 각 화면마다 이런 처리를 해주는 것은 꽤나 시간이 소요되는 작업이었습니다. 하지만 ViewModel 을 사용하면 이런 처리가 상당부분 간소화 됩니다.

이미 안드로이드 개발환경을 사용하신다면 아래 이미지를 통해 ViewModel 의 동작 컨셉을 쉽게 아실 수 있을겁니다.

Activity 와 ViewModel 의 수명 주기

ViewModel은 Activity 가 종료(Finish) 상태가 되기 전까지는 유지되며, 종료 상태가 될 때 onCleared() 를 호출하며 소멸됩니다. 따라서 ViewModel 에서 사용하는 UI 데이터를 onCleared() 를 통해 정리만 해주면 activity 의 생명 주기에서 자유롭게 데이터를 관리할 수 있습니다.

.

.

ViewModel 예제 – ViewModel 과 Factory class

ViewModel 구현 예제는 GitHub – ViewModelExample 에서 보실 수 있습니다.

Activity 를 정의한 MainActivity.kt 파일과 ViewModel을 정의한 MainViewModel.kt 파일을 위주로 보시면 됩니다.

https://github.com/godstale/Android-AAC—Basic-examples/tree/master/ViewModelExam

예제 파일은 아래처럼 click count 를 0부터 사용자가 click 할 때마다 1씩 증가해서 출력하도록 되어 있습니다. ViewModel 을 사용해서 화면이 회전되어도 click count 가 reset 되지 않도록 하는 것이 목표입니다.

주의!! ViewModel 사용을 위해서는 app 모듈의 build.gradle 에서 dependencies 를 추가해줘야 합니다. 그렇지 않은 경우 ViewModelProvider 클래스 참조할 때 에러가 발생할 것입니다.

dependencies {
    ......
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
}

먼저 ViewModel 의 구현부부터 확인해 보겠습니다. MainViewModel.kt 파일에는 3개의 클래스가 있습니다.

  • MainViewModel
    파라미터 없이 인스턴스를 간단하게 생성할 수 있는 ViewModel 을 정의한 클래스입니다. androidx.lifecycle.ViewModel 을 상속받습니다.
    내부에 count 변수를 두고 사용자가 버튼을 클릭하면 1씩 증가합니다. getCountText() 를 호출하면 카운트 횟수를 포함한 문자열을 반환합니다.
class MainViewModel : ViewModel() {
    val TAG = "MainViewModel"
    private var count = 0

    fun getCountText(): String {
        return "click count : $count"
    }

    fun clickButton(): String {
        return "click count : ${++count}"
    }

    override fun onCleared() {
        Log.d(TAG, "## MainViewModel - onCleared() called!!")
        Log.d(TAG, "## count = $count")
        super.onCleared()
    }
}
  • CustomViewModel
    MainViewModel 과 동일하게 동작하지만 인스턴스를 생성할 때 초기 count 값을 파라미터로 받도록 되어 있습니다.
  • CustomViewModelFactory
    CustomViewModel 인스턴스를 생성할 때 적절한 파라미터가 전달될 수 있도록 하기위해 정의한 custom factory 클래스입니다.
class CustomViewModel(var count: Int) : ViewModel() {
    val TAG: String = "CustomViewModel"

    fun getCountText(): String {
        return "click count : $count"
    }

    fun clickButton(): String {
        return "click count : ${++count}"
    }

    override fun onCleared() {
        Log.d(TAG, "## CustomViewModel - onCleared() called!!")
        Log.d(TAG, "## count = $count")
        super.onCleared()
    }
}

class CustomViewModelFactory(val count: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(CustomViewModel::class.java)) {
            return CustomViewModel(count) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

ViewModel 을 상속한 클래스는 Activity가 종료되는 시점에 호출되는 onCleared() 함수를 구현해줘야 합니다. 당연히 여기에는 변수나 리소스 해제 작업을 해주면 됩니다.

ViewModel 클래스가 인스턴스를 생성할 때 파라미터를 받도록 하기 위해서는 ViewModelProvider.Factory 를 상속해서 create() 를 구현해줘야 합니다. create() 함수 안에는 원하는 ViewModel 인스턴스(여기서는 CustomViewModel)를 생성해서 리턴해주면 됩니다.
(당연히 CustomViewModel 은 생성자 또는 기타의 방법으로 해당 parameter를 처리하도록 작성해야 합니다.)

주의!! ViewModel 은 Activity의 생명 주기 외에서 동작하므로 ViewModel에서 Context, View 또는 Lifecycle 등을 참조해서는 안됩니다. Context 가 필요한 경우 Activity context 대신 Application context를 사용할 수 있으며, 이때는 AndroidViewModel 을 상속해서 구현하면 됩니다.

.

.

ViewModel 예제 – Activity 코드

ViewModel 이 준비되었으니 Activity 에서 해당 ViewModel 을 생성해서 사용하면 됩니다. 예제에 있는 MainActivity 에서는 2개의 ViewModel 을 생성해서 연결합니다.

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    lateinit var customViewModel: CustomViewModel
    lateinit var viewModelFactory: CustomViewModelFactory

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 1. create basic view model
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        // 2. create custom view model
        viewModelFactory = CustomViewModelFactory(0)
        customViewModel = ViewModelProvider(this, viewModelFactory).get(CustomViewModel::class.java)

        // 3. init UI
        text_desc1.text = viewModel.getCountText()
        text_desc2.text = customViewModel.getCountText()

        // 4. update UI
        button_class.setOnClickListener {
            text_desc1.text = viewModel.clickButton()
            text_desc2.text = customViewModel.clickButton()
        }
    }
}

onCreate() 에서 파라미터 없는 ViewModel 을 생성해서 연결하는 코드입니다.

주의!! 구버전 androidx.lifecycle:lifecycle-extensions:2.0.0 에서 사용하던 ViewModelProviders.of(this) 코드는 deprecated 되었습니다. androidx.lifecycle:lifecycle-extensions:2.2.0 이후로는 아래 방법을 사용해야 합니다.

        // 1. create basic view model
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

파라미터가 필요한 CustomViewModel 은 해당 인스턴스를 생성할 수 있도록 구현된 factory 클래스를 사용해서 아래처럼 생성합니다.

        // 2. create custom view model
        viewModelFactory = CustomViewModelFactory(0)
        customViewModel = ViewModelProvider(this, viewModelFactory).get(CustomViewModel::class.java)

ViewModel 이 생성되면 UI 초기화 및 click event listener 처리 작업을 해주면 됩니다.

        // 3. init UI
        text_desc1.text = viewModel.getCountText()
        text_desc2.text = customViewModel.getCountText()

        // 4. update UI
        button_class.setOnClickListener {
            text_desc1.text = viewModel.clickButton()
            text_desc2.text = customViewModel.clickButton()
        }

이후 예제를 실행해보면 버튼 click 이벤트에 따라 count 가 증가되고, 가로-세로 화면 전환에도 count 값이 초기화되지 않음을 알 수 있습니다.

앱을 종료하고 Log 를 확인해보면 Activity에서 사용했던 2개의 ViewModel 이 모두 연결 해제되어 onCleared() 가 2번 찍힌다는 것을 알 수 있습니다.

.

.

ViewModel 예제 – 활용

ViewModel 은 Activity/Fragment의 생명 주기에 대한 처리를 위해 고안된 전통적인(?) 해법에 기반하고 있습니다.

화면 전환과 같이 Activity가 종료되는 상황에서, 앱이 종료되기 전 호출되는 onSaveInstanceState() 를 이용한 방법은 직렬화 하기 힘든 객체(이미지 등…)에는 사용하기 힘들기 때문에 유보된 프래그먼트(Retained Fragment, UI가 없는 프래그먼트)를 사용했습니다. ViewModel 도 이와 유사한 방식으로 처리하며 보다 자세한 동작 방식은 아래에서 확인할 수 있습니다.

ViewModel 의 장점을 정리하자면 아래와 같습니다.

  • Activity 의 생명 주기와 연동되어 동작하므로 UI 데이터 관리, 리소스 해제가 간편
  • MVVM 패턴 구현이 용이
  • Activity(Fragment) : ViewModel 이 1:N, N:N 관계 설정 가능
    => 여러 fragment 에서 동일한 ViewModel 을 참조하고 싶다면 동일한 ViewModelStore 를 설정하면 됨
ViewModelProvider(activity.viewModelStore).get(MainViewModel::class.java)

참고