[Android] ViewModelProvider와 by viewModels에 대해.

Janzizu·2023년 10월 4일
0
post-thumbnail

안드로이드 개발을 할 때 viewmodel을 사용해 액티비티와 프래그먼트 같은 UI 컴포넌트의 수명주기와 연결하여 데이터를 보관하고 관리하도록 한다.
다들 알다시피 viewmodel을 이용하면 화면에 대한 구성이 변경되더라도 (화면회전과 같은) 데이터가 유지되며, UI 컴포넌트와 분리된 방식으로 비즈니스 로직들을 처리할 수 있다.

🔎 viewmodel에서 데이터가 유지될 수 있는 이유

그럼 뷰모델은 어떻게 액티비티와는 다르게 화면회전과 같은 구성이 변경되어도 데이터를 유지할 수 있을까?

우선 뷰모델은 액티비티 혹은 프래그먼트와는 다르게 다양한 생명주기를 갖고 있지 않고,
변하지 않는다.
(액티비티 or 프래그먼트가 여러 생명주기를 갖고 변화하지만 뷰모델은 하나의 scope로 일관되게 유지되며 액티비티의 ondestroy가 호출되고 나서야 oncleared가 호출됨)

뷰모델 프로세스는 다음과 같다.

  • viewModelProvider를 통해 viewModel인스턴스를 요청함
  • viewModelProvider내부에서는 viewModelStoreOwner를 참조하여 viewModelStore를 가져옴 (viewModelStore는 owner를 통해 관리)
  • viewModelStore에게 이미 생성된 viewModel 인스턴스를 요청
  • 만약 viewModel인스턴스를 갖고 있지 않다면 팩토리를 통해 생성
  • 생성한 viewModel인스턴스를 ViewModelStore에 저장하고 클라이언트에게 반환
  • 요청이 들어올 때 마다 위의 1~3번째줄 반복...

viewModelStore는 viewModel들을 담고있는 클래스이며,
내부적으로 map으로 구현되며, owner에 의해 관리되므로
owner(액티비티 혹은 프래그먼트)의 생명 주기가 완전히 destroy되지 않는 이상 동일한 viewModelStore를 가지고,
viewModelStore가 viewModel인스턴스를 관리하기 때문에 데이터 손실이 없게 된다.


ViewModelProvider

ViewModelProvider는 단어만 보아도 viewModel을 제공해줄 것만 같다.

ViewModelProvider는 Android Jetpack의 ViewModel 라이브러리에서 제공하는 클래스로, ViewModelProvider를 사용하면 ViewModel 인스턴스를 생성하고 관리할 수 있다.

ViewModelProvider는 ViewModel 인스턴스를 생성하기 위한 팩토리 역할을 하며,주로 액티비티나 프래그먼트 내에서 ViewModelProvider 객체를 통해 viewmodel 인스턴스를 얻는다.

일반적인 사용법은 아래와 같다.

val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

ViewModelProvider를 통해 viewmodel 인스턴스를 얻는 것을 확인할 수 있다.
위 코드에서 this는 LifecycleOwner로서, 해당 생명주기에 맞추어 viewmodel이 관리된다.

하지만 위의 코드보다 아래와 같이 작성하면 더 간결하게 viewmodel을 얻을 수 있다.

private val viewModel: MainViewModel by viewModels()

🎇 위임 (Delegate)

by viewModels() 에 들어가기에 앞서...

간략하게 코틀린에서 위임 패턴에 대해 알아보자!
어떠한 기능을 자신이 수행하지 않고 다른 객체가 수행하도록 하는 패턴이다.
기능을 위임시킨다고 볼 수 있다.

by lazy

by 키워드를 쓸 떄 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에 대한 정의를 나타내는데, 지연 초기화를 위한 기능을 제공하고 있다.
Lazy는 제네릭 인터페이스로 타입 매개변수 T에 따라 지연초기화 되는 값의 타입이 결정되며, value 프로퍼티는 Lazy 인스턴스의 지연 초기화된 값을 가져온다.

프로퍼티
value는 값이 한 번 초기화되면, 해당 값은 인스턴스의 나머지 수명 동안 변경되지 않아야 한다.
메서드
isInitilized() 는 Lazy인스턴스에 대해 값이 이미 초기화 되었는지 여부를 반환한다.

internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer!!()
                initializer = null
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}  

내부 Lazy 인터페이스에 대한 구현체를 보면 지연 초기화를 제공하고 필요한 시점에 값을 계산하고 저장하도록 한다.

  • 초기화 되지 않은 상태를 나타내는 _value 변수
  • value는 지연된 값을 반환한다.

어쨌든 그냥 by lazy는 값을 나중되어서 필요할 때 계산하기 위한 것이라고 생각하면 된다.


by viewModels()

아래와 같이 build.gradle app수준에 아래와 같이 의존성을 추가해주고

    implementation 'androidx.activity:activity-ktx:${version}'
    implementation 'androidx.fragment:fragment-ktx:${version}'
private val viewModel: MainViewModel by viewModels()

위와같이 viewModel을 쉽게 만들어 사용할 수 있다.

by viewModels()는 Android Jetpack의 viewmodel 라이브러리에서 제공하는 kotlin 확장함수로, 이 함수를 통해 간단하게 viewmodel인스턴스를 생성할 수 있다.

코드에서 MainViewModel은 해당하는 (가져올) viewModel 클래스이며,
viewModels() 함수는 Lazy<VM> 타입의 프로퍼티 위임을 반환한다.
이렇게 함으로써, 해당 프로퍼티에 접근할 때 viewmodel 인스턴스가 생성된다.

by 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 }
    )
}

위 코드 역시 위에서 본 바와 같이 내부에서 Lazy를 사용하고 있다.
따라서 해당 ViewModel에 접근할 때까지 초기화를 지연시키며 이렇게 함으로써 필요한 시점에만 ViewModel이 생성되고 초기화된다.

참고
https://charlezz.medium.com/viewmodel%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-viewmodel-%EC%B4%88%EB%B3%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C-e1be5dc1ac18

0개의 댓글