ViewModel 인스턴스를 얻는 방법으로는 ViewModelProvider().get과 by viewModels() 두 가지 방법이 있다.
ViewModelProvider().get으로 ViewModel 인스턴스를 얻는 방법은 이 글에서 살펴보았다. 그런데 by viewModels()를 사용하여 인스턴스를 get하는 방법은 내부적으로 어떤 차이가 있을지 궁금해서 살펴보았다.
// ImageDetailActivity.kt
private val viewModel: ImageDetailViewModel by viewModels()
먼저 위의 예시를 가지고 액티비티에서 by 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 }
)
}
by viewModels()의 viewModels()는 ComponentActivity의 확장함수이다. ViewModelLazy 객체를 리턴한다. ViewModelLazy의 생성자로 VM::class, { viewModelStore }이 전달되는데 이를 통해 ViewModel 클래스와 액티비티의 viewModelStore가 전달된다는 것을 알 수 있다. 위 예시에서는 ImageDetailViewModel 클래스와 ImageDetailActivity의 viewModelStore가 인자로 전달될 것이다.
또한 현재 예시에서는 extrasProducer와 factoryProducer 둘다 default 값인 null이므로 defaultViewModelProviderFactory와 defaultViewModelCreateExtras가 전달된다.
참고로 여기서 리턴되는 ViewModelLazy 객체는 지연초기화를 위한 인터페이스인 Lazy 인터페이스의 구현 클래스이다. 잠깐 Lazy 인터페이스를 살펴보자.
/**
* Represents a value with lazy initialization.
*
* To create an instance of [Lazy] use the [lazy] function.
*/
public interface Lazy<out T> {
/**
* Gets the lazily initialized value of the current Lazy instance.
* Once the value was initialized it must not change during the rest of lifetime of this Lazy instance.
*/
public val value: T
/**
* Returns `true` if a value for this Lazy instance has been already initialized, and `false` otherwise.
* Once this function has returned `true` it stays `true` for the rest of lifetime of this Lazy instance.
*/
public fun isInitialized(): Boolean
}
Lazy 인터페이스는 지연 초기화 값을 표현하기 위한 인터페이스이며, 추상 프로퍼티 value와 추상 메서드 isInitialized()가 선언되어 있다.
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 factory = factoryProducer()
val store = storeProducer()
ViewModelProvider(
store,
factory,
extrasProducer()
).get(viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
}
다시 돌아와서... 그래서 Lazy 인터페이스를 구현한 ViewModelLazy 클래스는 value와 isInitialized()를 override하고 있다. 커스텀 게터를 살펴보면 현재 캐시된 viewModel이 존재하지 않으면, ViewModelProvider().get을 통해서 ViewModel을 인스턴스를 get하고 존재하면 기존에 캐시된 viewModel을 return하는 것을 알 수 있다.
그러니까 by viewModels()의 내부적으로 보면 지연 초기화를 했을 뿐이고,
private val viewModel = ViewModelProvider(this).get(ImageDetailViewModel::class.java)
사실상 이렇게 코드를 작성한 것과 크게 다를 바는 없다.
by viewModels()가 Lazy<VM> 구현 객체를 리턴하는 것에서 알 수 있듯이, 사용하는 이유는 지연 초기화를 하기 위함이다. 지연 초기화는 초기화 작업을 미루다가 필요할 때 진행하는 방법이다. 이 방법을 사용함으로서 ViewModel 인스턴스가 불필요하게 생성되는 것을 방지하여 메모리 낭비를 줄일 수 있다.
by viewModels()를 예시로 들었지만, 다른 지연 초기화도 이와 같은 이점을 얻기 위해서 사용하는 것이다. 공통적으로 메모리 낭비를 줄여서 퍼포먼스를 향상시키기 위해서 지연 초기화를 한다. 또한 변수를 선언했지만 이에 대한 초기화를 나중에 할 수 밖에 없는 경우(예를 들면 프로퍼티를 통한 의존성 주입)에도 지연 초기화를 한다.
액티비티에서의 코드를 예시로 들었지만 프래그먼트에서의 by viewModels() 사용도 내부적으로 보면 액티비티에서 사용했을 때의 흐름과 크게 다른 것은 없다. 프래그먼트에서 by viewModels()를 따라가보면, 결국에는 똑같이 Lazy<VM> 인터페이스를 구현하는 ViewModelLazy 객체를 리턴한다는 것을 알 수 있다.