[Android] ViewModel

혜령·2022년 2월 22일
2

Android 공부하기

목록 보기
7/8

ViewModel이란?

ViewModel은 MVVM패턴에서 등장하였습니다. 이 개념은 앱 개발에서만 한정되는 개념이 아닙니다. View와 Model 사이에서 매개체 역할을 하고, View의 데이터를 관리하는 역할을 하는 것을 ViewModel이라고 합니다.

이 글에서 알아볼 ViewModel은 Android Architecture Components(AAC)에 속하는 ViewModel입니다. 안드로이드의 ViewModel은 수명주기를 고려하여 UI와 관련된 데이터들을 저장하고 관리하는 것을 돕게 됩니다. AAC-VM과 MVVM패턴의 ViewModel이 같은 것이 아니라, AAC-VM을 이용하여 MVVM패턴의 ViewModel을 구현할 수 있는 것입니다.

ViewModel을 사용하여 얻을 수 있는 이점을 아래에서 계속 살펴보겠습니다.

ViewModel 등장 배경

Google에서 안드로이드 앱 개발에 맞게 MVVM패턴을 설계하도록 라이브러리를 제공하는데, AAC-VM이 바로 그 라이브러리 중 하나이고, 이것을 이용해서 안드로이드 개발을 하는 것이 좋습니다.

안드로이드 개발을 할 때, Activity나 Fragment가 다시 생성되는 경우에 여기에 저장된 모든 UI 데이터들은 사라지게 됩니다. 이 부분은 개발자가 직접 저장해서 유지되도록 해줘야 하는데, 간단한 경우에는 onSaveInstanceState()를 사용합니다. 하지만 대용량에서는 적합하지 않습니다.

또한 Activity나 Fragment에서 비동기 호출을 하게 되는데, 이때 신경을 써줘야 하는 부분들이 많이 있습니다. 이런 작업들을 모두 Activity나 Fragment에서 처리한다면, 과도한 책임이 할당되게 됩니다. 따라서 View 데이터를 다루는 로직을 따로 관리하는 것이 좋습니다. 그렇게 해서 등장한 것이 바로 ViewModel입니다.

ViewModel의 목적은 UI 컨트롤러의 데이터를 캡슐화하여 구성이 변경되어도 데이터를 유지하는 것입니다. 두번째로는 UI에 과도한 책임을 맡기게 되면 테스트와 유지보수에 어려움이 있습니다.

따라서 UI 데이터를 소유하고 관리하는 로직을 ViewModel로 옮기게 된 것입니다.

UI 상태저장

ViewModel의 가장 큰 목적은 UI 상태저장입니다. 사용자는 실행하는 작업에 따라서 보존되기를 바라는 데이터가 있을 것입니다. 하지만 어떠한 경우에는 시스템은 그런 상태를 유지하지 않는 경우도 있습니다.

전화를 받고 다시 돌아오거나, 화면을 회전하는 경우를 예로 들 수 있습니다. 이런 경우는 Activity가 onPause() → .. → onDestory() 콜백 메서드가 호출되고, 다시 onCreate()→ .. → onResume() 콜백 메서드가 호출되는 것입니다. 즉, onDestroy가 호출되면서 Activity의 인스턴스가 소멸되면서 여기에 저장된 데이터들도 모두 사라지는 것입니다.

이런 경우에 데이터를 다시 받아와서 사용자의 기대를 만족시켜야 합니다. 간단한 데이터는 onSaveInstanceState()를 이용해서 저장해서 해결합니다. 하지만 대용량의 데이터는 이것을 이용할 수 없습니다. 그래서 비동기 호출을 통해서 다시 데이터를 받게 되면 시간이 걸리고 리소스가 낭비됩니다. 이런 경우에 ViewModel을 사용하면 됩니다.

ViewModel은 메모리에 데이터를 보관하므로 디스크나 네트워크 데이터보다 검색을 빠르게 할 수 있습니다. 또한 Activity(혹은 다른 lifecycleOwner)의 생명주기를 따르게 때문에 그동안 데이터가 유지되고, 구성변경 후에 자동으로 연결됩니다.

onSaveInstanceState()

그렇다면 ViewModel을 사용해서 UI 상태를 저장하면 onSaveInstanceState()는 사용하지 않아도 되는 걸까요?

정답은 아닙니다. 제대로 된 UI 상태 저장을 위해서는 ViewModel과 onSaveInstanceState를 같이 사용해야 합니다. ViewModel은 기기의 구성 변경(화면 회전)에서는 값이 유지되지만, 프로세스가 중단(다른 앱으로 이동했다가 돌아오는 경우에 발생할 수 있음)되면 같이 폐기되기 때문입니다. 그에 비해서 onSaveInstanceState는 이렇게 비정상 종료되는 경우 값이 복원됩니다.

사용자는 잠시 다른 앱을 사용하는 경우에 앱 프로세스는 백그라운드에 배치됩니다. 따라서 시스템은 프로세스를 폐기할 수도 있습니다. 이런 경우에도 사용자의 앱 상태가 유지되기를 바라는 기대를 만족시키기 위해서 프로세스의 중단에도 유지가 되는 onSaveInstanceState를 사용하게 됩니다.

Lifecycle

안드로이드 프레임워크에서는 Activity와 Fragment 같은 UI 컨트롤러의 수명 주기를 관리합니다.

ViewModel의 생명주기는 ViewModelProvider의 생성자 함수에 전달되는 ViewModelStoreOwner의 수명주기로 지정됩니다. Activity라면 Activity가 finish()될때까지, Fragment는 Fragment가 분리될 때까지 메모리에 남게 됩니다.

Activity를 예로 들어서 생명주기를 살펴보겠습니다.

시스템에서 Activity의 onCreate()가 호출될 때, ViewModel을 요청하고, finish()가 호출되고 폐기될 때까지 ViewModel은 존재하게 됩니다.

여기서 기기 화면이 회전되는 경우를 보면 onCreate()가 다시 호출되는 것을 알 수 있는데, ViewModel은 계속 살아있습니다. 즉, Activity의 인스턴스가 사라지고 다시 생성되어도 ViewModel은 남아있습니다. 따라서 화면 회전 시에도 데이터를 보존할 수 있는 것입니다.

ViewModel 클래스의 메서드는 onCleared() 콜백 메서드 하나 뿐입니다. ViewModel이 더 이상 사용되지 않고 소멸되는 순간에 호출됩니다. ViewModel에 메모리릭이 날 수 있는 작업이 있다면 onCleared()에서 작업을 끝내주어 메모리릭을 방지할 수 있습니다.

이에 대한 예는 아래 블로그를 통해서 확인할 수 있습니다.

[Android] ViewModel의 onCleared() 활용법

Fragment간의 데이터 공유

Activity에 둘 이상의 Fragment가 존재하는 할 때, Fragment간 데이터가 공유되야하는 경우가 많습니다. 이 작업은 간단한 작업이 아니였지만, ViewModel을 사용하면 간단하게 처리할 수 있습니다.

두 Fragment에 같은 ViewModel 객체를 생성해야 합니다. 그러려면 Fragment에 FragmentScope가 아닌, 동일한 ActivityScope로 ViewModel을 생성하면 됩니다. 즉, ViewModelStoreOwner가 서로 같은 Activity라면 싱글톤처럼 같은 ViewModel을 공유하게 되고, 다른 Activity라면 서로 다른 객체가 됩니다.

Fragment는 다른 Fragment의 동작이나 생명주기를 신경 쓸 필요가 없이 데이터를 공유할 수 있게 됩니다.

주의 사항

ViewModel에서 Activity의 Context를 참조하는 클래스를 참조해서는 안됩니다. 여기에는 대표적으로 Activity나 Fragment, View를 예로 들 수 있습니다. ViewModel의 생명주기가 Activity보다 길기 때문에, 참조를 하고 있다면 Activity가 종료된 이후에도 GC가 수거를 할 수 없기 때문입니다. 위에서 언급한 것과 같이 Activity 인스턴스는 회전이 되면 소멸됐다가 다시 생성됩니다. 만일 이 Activity를 참조하고 있다면 메모리릭이 발생하는 것입니다. Activity는 메모리를 많이 차지하기 때문에 메모리릭이 발생하면 안됩니다.

또한 ViewModel의 목적은 UI 데이터를 저장하고 관리하는 것입니다. 여기에 자신이 어떤 뷰에 세팅되는지에 대한 것은 나타낼 필요가 없습니다. 그런 코드가 없이 클래스를 최대한 독립적이게 구현해야 합니다.

ViewModel Process

ViewModelProvider 클래스는 UI 컨트롤러에게 ViewModel 클래스를 제공하여 두 클래스를 연결하는 기능이 구현되어 있는 클래스입니다.

이 클래스가 제공하는 기능을 통해서 Activity(혹은 Fragment)와 우리가 정의한 ViewModel 클래스를 연결하고, ViewModel에 정의된 데이터를 Activity로 가져오게 됩니다.

더 자세하게는 ViewModel 클래스를 상속하여 정의한 클래스는 생성자를 통해서 인스턴스화 할 수 없고, ViewModelProvider.Factory 인터페이스를 필요로 합니다.

우리가 만든 ViewModel은 ViewModelStore에서 관리를 합니다. 내부적으로 HashMap를 가지고 ViewModel를 관리합니다.

ViewModelStore는 ViewModelStoreOwner에서 관리를 합니다. ViewModelStoreOwner 인터페이스는 ComponentActivity와 Fragment가 Implement합니다. 따라서, 우리가 ViewModel 객체를 생성할 때 Activity나 Fragment가 필요로 하고, 그것으로 ViewModel의 Scope가 결정되는 것입니다.

by ViewModels

ViewModel을 초기화하는 방법은 2가지가 존재합니다.

첫번째 방법은 위에서 살펴본 ViewModelProvider을 직접 사용하는 방식이고, 두번째 방법은 by viewModels()를 사용하는 방식입니다.

// 1번
val model: MyViewModel by viewModels()
// 2번
val model: MyViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

2가지 방법은 모두 같은 동작을 수행합니다.

1번 방식은 Activity KTX모듈을 포함하면 지원하는 기능입니다. Kotlin의 1번 방식은 Lazy Initialization을 하는 이점이 존재하기 때문에 1번 방식으로 초기화하는 것이 권장됩니다.

추가적으로 인자를 가진 초기화 방법은 아래 블로그를 통해서 확인할 수 있습니다.

[Android] 한눈에 보는 ViewModel 초기화 방법 A to Z

구현 방법

  1. 종속성 추가

    // build.gradle 파일(프로젝트 수준)
    allprojects {
        repositories {
            google()
        }
    }
    
    // build.gradle 파일(앱 모듈 수준)
    dependencies {
        def lifecycle_version = "2.2.0" 
        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    }
  2. ViewModel 클래스를 상속받은 나의 ViewModel 클래스 만들기

    class MyViewModel : ViewModel() {
        private val users: MutableLiveData<List<User>> by lazy {
            MutableLiveData<List<User>>().also {
                loadUsers()
            }
        }
    
        fun getUsers(): LiveData<List<User>> {
            return users
        }
    
        private fun loadUsers() {
            // Do an asynchronous operation to fetch users.
        }
    }
  3. Activity에서 사용

    class MyActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            // Create a ViewModel the first time the system calls an activity's onCreate() method.
            // Re-created activities receive the same MyViewModel instance created by the first activity.
    
            // Use the 'by viewModels()' Kotlin property delegate
            // from the activity-ktx artifact
            val model: MyViewModel by viewModels()
            model.getUsers().observe(this, Observer<List<User>>{ users ->
                // update UI
            })
        }
    }
  4. Fragment에서 사용

    class SharedViewModel : ViewModel() {
        val selected = MutableLiveData<Item>()
    
        fun select(item: Item) {
            selected.value = item
        }
    }
    
    class ListFragment : Fragment() {
    
        private lateinit var itemSelector: Selector
    
        // Use the 'by activityViewModels()' Kotlin property delegate
        // from the fragment-ktx artifact
        private val model: SharedViewModel by activityViewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            itemSelector.setOnClickListener { item ->
                // Update the UI
            }
        }
    }
    
    class DetailFragment : Fragment() {
    
        // Use the 'by activityViewModels()' Kotlin property delegate
        // from the fragment-ktx artifact
        private val model: SharedViewModel by activityViewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
                // Update the UI
            })
        }
    }

Reference

https://developer.android.com/topic/libraries/architecture/viewmodel

https://choheeis.github.io/newblog//articles/2021-02/viewModel

https://stackoverflow.com/questions/65352324/view-model-initialization-using-by-viewmodels-vs-viewmodelproviderthis-ge

https://charlezz.medium.com/viewmodel이란-무엇인가-viewmodel-초보를-위한-가이드-e1be5dc1ac18

https://leveloper.tistory.com/216

profile
안녕하세요

0개의 댓글