액티비티 위에 프레그먼트가 하나 이상 올라가 있을 때, 서로 동일한 데이터를 공유해야 하는 경우가 자주 있다. AAC ViewModel을 활용하면 UI 컨트롤러 간 데이터 공유를 쉽게 처리할 수 있다.

 

일반 ViewModel과 달리 AAC ViewModel 객체를 생성하려면 ViewModelProvider를 사용해야 한다. 생성자 원형은 다음과 같다.

 

여기서 ViewModelStore는 HashMap으로 ViewModel 객체를 관리하는 저장소 역할을 한다. ViewModelStoreOwner는 이 ViewModelStore을 소유하는 역할을 하는 인터페이스다. ViewModelStoreOwner의 구현체는 ViewModelStore를 가지고 있다가 필요한 시점에 적절하게 복원하거나 파괴해야 한다.

 

이 ViewModelStoreOwner의 대표적인 구현체가 바로 Activity와 Fragment다. ViewModel을 선언하는 클래스가 다르더라도 생성자로 넘긴 ViewModelStoreOwner 객체가 같다면 ViewModelProvider는 동일한 ViewModel을 반환하게 된다.

 

무조건 공유가 가능한건 아니다

AAC ViewModel은 싱글톤 패턴으로 구현되어 있는데, activity가 destroy 되거나, fragment가 detached 될 때까지 유지된다. 즉 하나의 lifecycle에 한해서만 유효한 싱글톤 객체다. 이 특징으로 인해

  • ViewModel을 서로 다른 Activity에서 선언하면 서로 다른 lifecycle을 따라가게 된다. 동일한 ViewModel 클래스여도 별개의 메모리 공간을 사용하는 객체가 된다.
  • 하나의 액티비티 위에 올라간 프레그먼트들 안에서, 가장 lifecycle이 긴 Activity가 owner가 되도록 ViewModel을 선언하면 하나의 ViewModel을 공유하게 된다.

 

공유하는 방법

공유할 ViewModel 정의

class MyViewModel : ViewModel() {
    // ...
}

 

Activity에서 선언하기

방법 1. Activity에선 owner로 자기 자신을 넘겨주면 된다.

private lateinit var myViewModel: MyViewModel
// ...
// context를 사용 가능한 시점에서 초기화
myViewModel = ViewModelProvider(this)[MyViewModel::class.java]

방법 2. androidx.activity 패키지에 정의된 함수를 사용할 수도 있다.

viewModel 초기화는 context를 사용 가능한 시점부터 가능하다. 때문에 viewModel을 프로퍼티로 선언할 땐 보통 지연 초기화를 하게 된다. viewModels()를 사용하면 lazy나 lateinit없이 간편하게 초기화를 할 수 있다.

val myViewModel: MyViewModel by viewModels()

 

Fragment에서 선언하기

방법 1. activity 객체를 FragmentActivity나 ViewModelStoreOwner로 형변환 해서 넘겨주면 된다.

// 동일한 표현
myViewModel = ViewModelProvider(activity as FragmentActivity)[MyViewModel::class.java]
myViewModel = ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]

방법 2. Fragment에 자체적으로 정의되어 있는 requireActivity()라는 메서드를 사용해도 된다.

myViewModel = ViewModelProvider(requireActivity())[MyViewModel::class.java]

requireActivity는 getActivity가 null일 경우 IllegalStateException을 던진다.

그래서 개인적으로는 이렇게 확장 함수를 하나 만들어서 사용하고 있다.

fun Fragment.getViewModelStoreOwner(): ViewModelStoreOwner = try {
    requireActivity()
} catch (e: IllegalStateException) {
    this
}
myViewModel = ViewModelProvider(getViewModelStoreOwner())[MyViewModel::class.java]

 

방법 3. fragment-ktx

fragment도 attached 된 후부터 viewModel 초기화가 가능하다.

fragment-ktx를 사용하면 lazy나 lateinit없이 한줄로 초기화할 수 있다.

 

사용하려면 dependency에 ktx를 추가해야 한다. (현재 기준 최신 버전 1.2.4)

implementation "androidx.core:core-ktx:${ktx_core_version}"

사용법은 간단하다. activityViewModels()를 호출하면 된다.

private val myViewModel: MyViewModel by activityViewModels()

 

이제 ViewModel을 통해 Activity, Fragment가 동일한 데이터를 공유할 수 있게 되었다.

 

scope가 겹치지 않을 때 ViewModel을 공유하는 건 불가능할까?

위에서 lifecycle이 달라지면 ViewModel 객체가 달라진다는 얘기를 했다. 그럼 activity끼리 viewModel을 공유하는 것은 완전히 불가능할까? 결론만 말하자면 가능은 하다. ViewModelFactory를 커스텀하면 된다.

아래는 ViewModelFactory를 싱글톤으로 만들어서 owner로 뭐가 들어오든 언제나 동일한 객체를 반환하게 하는 예제다.

class MyViewModel : ViewModel() {

    companion object {
        private var instance: MyViewModel? = null
        fun getInstance() = instance ?: synchronized(MyViewModel::class.java) {
            instance ?: MyViewModel().also { instance = it }
        }
    }
}
class SingleViewModelFactory : ViewModelProvider.NewInstanceFactory() {

    companion object {
        private var instance: SingleViewModelFactory? = null
        fun getInstance() = instance ?: synchronized(SingleViewModelFactory::class.java) {
            instance ?: SingleViewModelFactory().also { instance = it }
        }
    }

    override fun <T : ViewModel?> create(modelClass: Class<T>) =
        with(modelClass) {
            when {
                isAssignableFrom(MyViewModel::class.java) -> MyViewModel.getInstance()
                else -> throw IllegalArgumentException("Unknown viewModel class $modelClass")
            }
        } as T
}
myViewModel = ViewModelProvider(this, SingleViewModelFactory.getInstance())[MyViewModel::class.java]

 

ViewModel의 기본적인 사용 목적은 UI Controller에서 보여줄 데이터를 관리하는 것이다. 서로 다른 scope에서 ViewModel을 공유하는 것은 ViewModel의 기본 사용 원칙에 어긋나는 안티 패턴에 해당된다. 데이터를 싱글톤으로 관리하고 싶다면 Repository 같은 다른 계층을 이용하는 것이 좋다.

 

결론 : 할 수 있지만 안하는게 좋다!

 


참고 링크