[Android] 뷰모델 초기화 방법 정리
뷰모델을 초기화하는 방법은 다양하다. 이 방법들 중에서 자신의 프로젝트, 뷰모델 성격에 맞는 초기화 방법을 사용하자.
ViewModelFactory, ViewModelProvider 사용
ViewModelProviders가 deprecated되서, 대신 ViewModelProvider를 사용해야 한다. 아래는 예시 코드다.
val viewModelFactory = MyViewModelFactory(myRepository)
val viewModel = ViewModelProvider(this, viewModelFactory).get(MyViewModel::class.java)
이 방법은 hilt를 사용하지 않는 경우, androidx.activity:activity-ktx 라이브러리를 사용하지 않는 경우에 적용할 수 있다.
activity-ktx 라이브러리를 사용하면 프로퍼티 위임 방식으로 뷰모델을 사용할 수 있기 때문에 번거로운 초기화 로직을 거치지 않아도 되는 장점이 있다.
by viewModels()
위에서 말한 activity-ktx 라이브러리를 추가할 경우 사용할 수 있는 프로퍼티 위임 방식이다.
위의 2줄짜리 코드를 viewModels()로 간결하게 표현한 형태다. 액티비티에서 viewModels()를 사용한 후, 내부 구현을 확인해 보면 아래와 같다.
@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline extrasProducer: (() -> CreationExtras)? = null,
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(
VM::class,
{ viewModelStore },
factoryPromise,
{ extrasProducer?.invoke() ?: this.defaultViewModelCreationExtras }
)
}
public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory,
private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val store = storeProducer()
val factory = factoryProducer()
val extras = extrasProducer()
ViewModelProvider.create(store, factory, extras)
.get(viewModelClass)
.also { cached = it }
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
}
ViewModelLazy란 클래스는 아래의 두 곳에서 사용하는 Lazy 구현이다. 그 외의 요소들에 대한 설명은 주석으로 써져 있으니 관심있다면 확인해보면 좋을 것이다.
- androidx.fragment.app.Fragment.viewModels
- androidx.activity.ComponentActivity.viewModels
그런데 무슨 일이 있어도 by를 써야 할까? 그건 아니다. ViewModelLazy 인스턴스를 가져오는 건 아래 형태로도 가능하다.
private val myViewModel = viewModels<MyViewModel>().value
이 방식도 충분히 사용 가능하지만, by viewModels()로 뷰모델을 사용하는 방식이 좀 더 일반적이다.
앞서 말한대로 by viewModels()는 ViewModelProvider를 좀 더 간결하게 쓰기 위한 프로퍼티 위임 방식이다.
이 방법으로 생성된 뷰모델은 그 뷰모델이 초기화된 액티비티 or 프래그먼트의 생명주기를 따른다. 때문에 뷰모델의 함수를 호출해서 뷰모델 안에 정의된 프로퍼티를 업데이트하더라도, 해당 프로퍼티의 값이 제대로 업데이트되지 않을 수 있다.
그리고 by viewModels 안에 requireParentFragment()를 넣을 수도 있다. 아래 코드는 안드로이드 디벨로퍼에 게시된 예시 코드다.
class ListFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel.
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
// Update the list UI.
}
}
}
class ChildFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel using the parent fragment's scope
private val viewModel: ListViewModel by viewModels({requireParentFragment()})
...
}
자식 프래그먼트와 부모 프래그먼트가 서로 같은 뷰모델을 공유해야 할 때 사용할 수 있다. 이걸 사용하면 부모 프래그먼트의 생명주기를 따르게 된다.
by activityViewModels
by가 있어서 프로퍼티 위임인 건 알겠는데 앞에 activity가 붙었다.
이 방식으로 뷰모델을 가져오게 되면 해당 액티비티로 범위가 지정된 뷰모델을 가져오게 된다.
예를 들어 어떤 액티비티에서 여러 프래그먼트를 사용하는데 하나의 뷰모델을 여러 프래그먼트에서 공유해서 써야 하는 상황이 올 수 있다. 이 때 위의 by viewModels()를 사용하면 앞서 말한대로 값의 업데이트가 제대로 이뤄지지 않는다.
프래그먼트의 특성 상 프래그먼트는 액티비티에 종속되는 UI기 때문에, 상위 컴포넌트로 범위가 지정된 뷰모델을 사용하게 되면 프래그먼트들은 같은 뷰모델을 바라보게 되고, 값의 업데이트도 정상적으로 이뤄진다.
ViewModelProvider.AndroidViewModelFactory
뷰모델 안에서 컨텍스트를 써야 하는 경우 사용한다. 그러나 뷰모델에서 컨텍스트를 갖거나 접근하는 것 자체를 일반적으론 권장하지 않는다. 이걸 만든 안드로이드도 쓰지 말라고 하고 있다.
https://developer.android.com/topic/architecture/recommendations?hl=ko
이유는 메모리 누수, 테스트 용이성의 저하, 커플링 때문이다. 정말 무슨 일이 있어도 불가피하게 필요한 경우가 아니라면 사용하지 말자.
아래는 사용 예시다.
val viewModel =
ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MyViewModel::class.java)