[안드로이드] AAC ViewModel 이란? 드디어 알아보자.

에짱·2021년 9월 24일
0

왜 필요할까?


Activity 나 Fragment 는 스크린 회전과 같은 configuration change 가 발생하면 Destroy 가 되고 다시 Create 되어 재생성됩니다. 그래서 화면에 있던 data 가 유지되지 않고 초기화 되지요. 예를 들어 단순하게 버튼을 눌러서 textView 로 숫자가 0부터 1씩 커지는 앱이 있다고 상상해봅시다. 세로 모드에서 버튼을 누르기 시작해서 5까지 올려놨습니다. 이때 가로모드로 바꿀 경우, 우리는 숫자가 5로 유지되길 원합니다. 하지만 화면이 다시 그려지기 때문에, 데이터를 저장해두지 않으면 초기화 되어서 0이 나오게 됩니다.

물론 이를 해결하기 위해서 activity 의 경우, onSaveInstanceState() 메서드를 사용해서 onCreate() 에서 번들에서 데이터를 유지할 수 도 있습니다만, 유저 리스트나, 비트맵과 같은 큰 데이터의 경우는 적합하지 않습니다.

따라서 그림처럼 activity 가 finish 될 때까지, 계속 살아있는 scope 를 가지는 ViewModel 에 데이터를 넣어두면 화면 회전이 되어도 데이터가 유지 될 수 있습니다.

ViewModel 이 뭐꼬?

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

ViewModel 공식 문서 첫 줄에 나와있는 정의입니다.
해석해보면, ViewModel 클래스는 라이프 사이클을 파악하여 UI 와 관련된 데이터를 저장하고 관리하기 만들어졌습니다. ViewModel 클래스는 스크린 회전과 같은 configuration change에서 데이터가 유지될 수 있도록 합니다.

위에서 설명한 필요성을 충족시키기 위한 녀석이라는 것이지요.

ViewModel 은 자신과 관련된 Activity 나 Fragment 에 보여줄(display) data(Configuration change 에도 살아남아야 하는) 를 가지고(hold) 있습니다. 즉, 데이터를 다루어서(ex. 계산 및 서버 통신 이후 받은 데이터 가공) UI Controller(Activiy, Fragment) 에게 보내주는 것이지요.

Activiy 나 Fragment 와 같은 UI controller 는 말 그대로 view 를 보여주거나, 사용자 input 을 받아오는 등의 UI 와 관련된 로직만을 담아야 합니다. 사용자의 input 을 받아서 데이터 처리를 하는 등의 로직은 모두 ViewModel 과 나머지에서 하도록 관심사 분리(separation of concern)를 해야 합니다.

어떻게 만들까?

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

기본적으로 ViewModelProvider 를 통해서 ViewModelFactory 가 ViewModel 객체를 객체화합니다. 생성자 파라미터가 있는 경우가 없는 경우 모두 가능합니다.
ViewModelProvider.get() 을 사용해서 ViewModelProvider 를 만들 수 있습니다.

//생성자 파라미터 없는 경우 1
//ViewModelProvider 에서 내부적으로 Default Factory 를 사용.

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        /* use ViewModelProvider's constructor provided from lifecycle-extensions package */
        noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
    }
}
//생성자 파라미터 없는 경우 2
//직접 Factory 를 생성해서 넣어주는 경우. 

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(NoParamViewModel::class.java)
    }
}
//생성자 파라미터가 있는 경우
//커스텀 Factory Class 를 직접 만들어서 넣어주어야 합니다. 
class HasParamViewModel(val repository: MainRepository) : ViewModel()

class HasParamViewModelFactory(private val repository: MainRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
            HasParamViewModel(repository) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

class MainActivity : AppCompatActivity() {
 
    private lateinit var hasParamViewModel: HasParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val repository = MainRepository()
 
        hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(repository))
            .get(HasParamViewModel::class.java)
    }
}

내부 동작 원리

뷰모델은 어떻게 configuration change 로 activity 가 재생성 되더라도 살아남을 수 있는 것일까요? 쾌락코딩님의 글을 참고해보면, viewModelStore 값을 configuration change 가 있더라도 유지/갱신시켜주는 것이 핵심인 것을 알 수 있습니다.

ViewModelProvider의 첫 번째 인자로 ViewModelStoreOwner를 넣어주는데, ComponentActivity가 ViewModelStoreOwner를 구현하고 있기 때문에 FragmentActivity와, 그의 하위 클래스인 AppCompatActivity 를 넣어주면 됩니다. ViewModelStoreOwner는 getViewModelStore()메서드 하나만 가진 인터페이스입니다.

처음에 ViewModel 을 생성할 때, getViewModelStore() 로 가져온 viewModelStore 에 저장하고 나중에 configuration 으로 다시 생성하려고 할 때 viewModelStore 에 같은 키값의 뷰모델이 있는 확인하고 있으면 해당 뷰모델을 갱신해주고 없으면 생성합니다.
따라서 viewModelStore 를 유지하는 것이 중요한데, 이는 Activity의 onStop과 onDestroy 사이쯤 시스템에 의해 호출되는 mLastNonConfigurationInstances 메서드에서 activity 의 일부 변수들을(viewModelStore) 저장해주는 역할을 해서 가능한 것입니다.

이렇게 ViewModel 은 관련된 UI controller 의 scope 가 살아있는 한 살아있습니다. 예를 들어 fragment 가 detached 되기 전까지, activiy 가 finish 되기 전까지 존재합니다. 그 이후에는 ViewModel 의 onCleared() 가 불리면서 사라집니다.

주의사항

ViewModel 에서는 fragments, activities, view 를 절대로 참조하면 안 됩니다. 이들은 configuration change 에서 사라지고 재생성되기 때문입니다.

그렇다면 viewModel 에서 불가피하게 context 를 참고해야 할 때, 어떻게 해야 될까?
안드로이드 제공하는
ViewModelProvider.AndroidViewModelFactory 를 사용하면 됩니다.

// 생성자 파라미터 없는 경우
class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)

//생성자 파라미터 있는 경우 커스텀 팩토리 필요
class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)

class HasParamAndroidViewModelFactory(private val application: Application, private val param: String)
    : ViewModelProvider.NewInstanceFactory() {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
            try {
                return modelClass.getConstructor(Application::class.java, String::class.java)
                    .newInstance(application, param)
            } catch (e: NoSuchMethodException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: IllegalAccessException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InstantiationException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InvocationTargetException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            }
        }
        return super.create(modelClass)
    }
}

또는 Koin 과 같은 의존성 주입으로도 해결이 가능합니다. 이는 나중에 의존성 주입을 공부하면서 알아보도록 하겠습니다! 아직은 잘 모름

MVVM 의 ViewModel 과의 차이?

AAC(Android Architecture Component) ViewModel은 Activity 내에서 1개만 생성가능합니다. 따라서 AAC ViewModel은 Activity안에서의 싱글톤 개념이기 때문에 Activity 내의 여러 Fragment를 가질시에 여러 Fragment에 각자의 ViewModel을 사용할 수 없습니다. 실제로 liveData 와 함께 사용할 때, 옵저빙이 되지 않는 것을 확인했습니다.
반면, MVVM 패턴에서 뷰와 뷰모델은 1:n 관계를 가질 수 있습니다.

참고자료

공식문서 : https://developer.android.com/topic/libraries/architecture/viewmodel?gclid=CjwKCAjwy7CKBhBMEiwA0Eb7ah-bhu63497KR19yeJCqeLRB7e7Xog5sMj8xf7ChV3xmV0ThbUgF8hoCfkwQAvD_BwE&gclsrc=aw.ds
codeLabs : https://developer.android.com/codelabs/kotlin-android-training-view-model?index=..%2F..android-kotlin-fundamentals#4
블로그 : (생성방법) https://readystory.tistory.com/176
(내부 원리) https://wooooooak.github.io/android/2020/10/11/AAC_VewModel_internal/
(MVVM ViewModel 과의 차이)https://medium.com/kenneth-android/android-mvvm-viewmodel%EA%B3%BC-aac-viewmodel%EC%9D%98-%EC%B0%A8%EC%9D%B4-8c0d54922e07

profile
지금 여기. Here and Now

1개의 댓글

comment-user-thumbnail
2024년 2월 26일

구글에서 뷰모델 관련글 여러글 중에 이 글이 가장 좋네요.
궁금한곳이 시~원하게 긁어졌습니다. 😊

답글 달기