Developing Myself Everyday
article thumbnail

 

사진: UnsplashApril Walker

 

 

 안드로이드 개발을 할 때, ViewModel을 사용하는 것은 이제 옵션이 아닌 필수일 정도로 ViewModel을 사용하는 것은 매우 당연해 졌습니다. 시간이 지나며 새로운 기술일이 나오게 되면서, 이에 대응하여 ViewModel의 사용 방법은 우리가 눈치채지도 못하게 자연스럽게 바뀌었습니다. 그러면서 모르고 지나갔던 부분들을 톺아보고자 이번 게시글에서는 ViewModel을 어떻게 사용하고 있는지 살펴보고자 합니다.

 

이 게시글은 3가지의 시리즈로 구성되어 있습니다.

 

(1) - ViewModel의 생성과 관리 [현재 게시글]

(2) - Hilt와 함께 ViewModel 가져오기

(3) - ViewModel과 SaveStateHandle 

 


 

 

ViewModel의 인스턴스


ViewModel을 사용하기 위해서 가장 먼저 해야할 일은 ViewModel의 인스턴스가 필요합니다. ViewModel의 인스턴스를 어떻게 사용할 수 있는지 알아보고자 합니다.

 

 

 

ViewModelProvider 생성


먼저 ViewModel의 인스턴스를 가져오기 위해서는, `ViewModelProvider`를 먼저 생성해야 합니다.

 

`ViewModelProvider`는 대부분 아래의 2가지의 생성자를 통해서 생성됩니다. 주된 차이는 ViewModel에 초기화 매개변수가 필요한 경우에 팩토리를 사용하느냐, 사용하지 않느냐에 있습니다. 

public open class ViewModelProvider

constructor(
    private val store: ViewModelStore,
    private val factory: Factory,
    private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) {
	
    ...
    
    public constructor(
        owner: ViewModelStoreOwner
    ) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

    public constructor(owner: ViewModelStoreOwner, factory: Factory) : this(
        owner.viewModelStore,
        factory,
        defaultCreationExtras(owner)
    )
    
    ...
    
}

 

 

`ViewModelStoreOwner`

`ViewModelProvider`를 생성하는데 가장 먼저 필요한 `ViewModelStoreOwner`는 말 그대로`ViewModelStore`를 소유(Own)하고 있는 인터페이스입니다.

interface ViewModelStoreOwner {

    /**
     * The owned [ViewModelStore]
     */
    val viewModelStore: ViewModelStore
}

 

 

그럼 `ViewModelStoreOwner`의 구현체는 무엇이냐.  바로 View(Activity, Fragment)입니다.

 

내부 코드를 파고 들다 보면 View들이 ViewModelStoreOwner을 상속하고 있으며 `ViewModelStore`를 가지고 있다는 것을 알 수 있습니다.

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        ContextAware,
        LifecycleOwner,
        ViewModelStoreOwner,
        ...
이런 구조로 되어 있기 때문에 지금까지 ViewModel은 Activity나 Fragment가 onCreate()된 이후에 가져올 수 있었습니다. 

 

 

 

ViewModelStore


`ViewModelStore`는 ViewModel의 핵심 요소로 ViewModel 인스턴스를 저장하고 관리하는 역할을 하며 주요 기능과 역할은 아래와 같습니다.

  1. ViewModel 관리: ViewModelStore는 ViewModel 인스턴스를 저장하고 해당 인스턴스의 생명주기를 관리합니다. 즉, ViewModel은 해당 ViewModelStoreOwner의 생명주기 동안 유지되며, 구성 변경(예: 화면 회전) 시에도 동일한 ViewModel 인스턴스를 재사용할 수 있습니다.
  2. ViewModel 인스턴스 생명주기: ViewModelStore는 ViewModel의 생명주기를 ViewModelStoreOwner와 연결합니다. 예를 들어, Activity나 Fragment의 onCreate() 메서드에서 ViewModel을 요청하면, ViewModelStore는 이미 존재하는 인스턴스를 반환하거나 새로운 인스턴스를 생성하여 반환합니다.

 

아래의 코드를 보면 ViewModel은 Map의 형태로 ViewModel을 특정할 수 있는 특수한 String과 함께 저장되며ViewModelStore을 통해 가져오고, 저장될 수 있다는 것을 알 수 있습니다.

open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()

    fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.onCleared()
    }

    operator fun get(key: String): ViewModel? {
        return map[key]
    }

    fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

 

 

 

심화: ViewModelStore 유지하기


Activity의 경우를 보자면 onCreate()될 때 Activity의 객체가 초기화 되고 onDestory() 될 때 객체가 소멸된다는 것을 이해하고 계실겁니다. 일반적으로 ViewModelStore 또한 Activity 객체가 소멸되고 다시 초기화할 때에도 같이 초기화될 것입니다. 

 

ViewModelStore는 계속 유지되어야 하기에, 이를 위해서 ActivityManager나 FragmentManager가 나섭니다. 

 

 

ViewModelStore 유지하기

구성 변경 시에도 ViewModel이 삭제되지 않고 유지되게 하기 위해서 `onRetainNonConfigurationInstance()` 메서드가 실행됩니다.

final override fun onRetainNonConfigurationInstance(): Any? {
    // Maintain backward compatibility.
    val custom = onRetainCustomNonConfigurationInstance()
    var viewModelStore = _viewModelStore
    if (viewModelStore == null) {
        // No one called getViewModelStore(), so see if there was an existing
        // ViewModelStore from our last NonConfigurationInstance
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            viewModelStore = nc.viewModelStore
        }
    }
    if (viewModelStore == null && custom == null) {
        return null
    }
    val nci = NonConfigurationInstances()
    nci.custom = custom
    nci.viewModelStore = viewModelStore
    return nci
}

 

이 메서드는 이전 상태(lastNonConfigurationInstance)에서 ViewModelStore를 복원하거나, 기존 상태를 새로 생성된 NonConfigurationInstances 객체에 포함시켜 반환합니다.

 

사용자 정의 상태는 `onRetainCustomNonConfigurationInstance()` 메서드에서 반환된 값을 통해 유지됩니다.

 

 

유지된 ViewModelStore 가져오기

아래는 ComponentActivity에 있는 ViewModelStore를 가져오는 메서드입니다. 이 메서드는 Activity가 onCreate되었다면 `ensureViewModelStore()` 메서드를 실행합니다.

override val viewModelStore: ViewModelStore
    get() {
        checkNotNull(application) {
            ("Your activity is not yet attached to the " +
                "Application instance. You can't request ViewModel before onCreate call.")
        }
        ensureViewModelStore()
        return _viewModelStore!!
    }

 

 

`ensureViewModelStore()` 메서드는 mViewModelStore 객체가 Null이라면 `lastNonConfigurationInstance`를 가져와서 여기에 저장되어 있는 viewModelStore로 초기화를 합니다.

private fun ensureViewModelStore() {
    if (_viewModelStore == null) {
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            _viewModelStore = nc.viewModelStore
        }
        if (_viewModelStore == null) {
            _viewModelStore = ViewModelStore()
        }
    }
}

 

 

NonConfigurationInstances는 아래의 메서드를 통해 가져올 수 있는데

@Nullable
public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
}

 

 

이 객체는 Activity가 ActivityManager에 의해 `attach`될 때 초기화됩니다.

final void attach(Context context, ActivityThread aThread,
        ...
        NonConfigurationInstances lastNonConfigurationInstances,
        ...
  		) {
        ...
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        ...

 

이런 과정을 통해서 Activity가 재구성될 때에도 ViewModelStore를 유지할 수 있습니다.

 

만약 이전에 생성된 ViewModelStore의 값이 없다면 `ensureViewModelStore()` 메서드를 보시면 알 수 있듯이 새로운 ViewModelStore 객체를 생성합니다.

 

 

 

get()을 통한 ViewModel 인스턴스 가져오기


이제는 `ViewModelStore`을 통해 저장된 ViewModel의 인스턴스를 가져오기만 하면, ViewModel을 사용할 수 있게 됩니다. 이는 이전에 생성한 ViewModelProvider의 `get()`을 통해 가능합니다.

public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName
        ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
    return get("$DEFAULT_KEY:$canonicalName", modelClass)
}

public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    val viewModel = store[key]
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel!!)
        return viewModel as T
    } else {
        @Suppress("ControlFlowWithEmptyBody")
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }
    val extras = MutableCreationExtras(defaultCreationExtras)
    extras[VIEW_MODEL_KEY] = key
    return try {
        factory.create(modelClass, extras)
    } catch (e: AbstractMethodError) {
        factory.create(modelClass)
    }.also { store.put(key, it) }
}

 

 

첫번째 `get()`은 전달된 modelClass를 기반으로 기본 키를 생성합니다. 만약 modelClass가 로컬 클래스나 익명 클래스일 경우 예외를 발생합니다. 

 

이렇게 생성한 키로 두번째 `get()`를 호출합니다. 이 메서드는 키와 클래스를 기반으로 ViewModel을 반환하거나 새로 생성합니다. 이 메서드에서 바로 `ViewModelStore의 get`을 통해 저장된 ViewModel의 인스턴스를 가져옵니다. 

 

'if (modelClass.isInstance(viewModel))'로 ViewModel의 인스턴스가 존재하는지 확인하고 만약 존재하지 않다면 'store.put(key, it)'을 통해 `ViewModelStore`에 저장합니다.

 

 

ViewModel 지속성의 내부 흐름

액티비티 생성:

  • ViewModelStore가 초기화됩니다.

ViewModel 요청:

  • ViewModelProvider ViewModelStore에서 기존 인스턴스를 확인합니다.
  • 만약 인스턴스가 없으면 새 인스턴스를 생성하고 저장합니다.

구성 변경 (Configuration Change):

  • ViewModelStore onRetainNonConfigurationInstance()를 통해 유지됩니다.
  • 새로운 액티비티는 유지된 ViewModelStore를 가져옵니다.

액티비티 소멸 (Activity Destruction):

  • 액티비티가 진짜로 소멸되면 (구성 변경이 아님) ViewModelStore와 그 안의 내용이 삭제됩니다.

 

 

 

by viewModels()


이렇게 ViewModel의 인스턴스를 얻을 수 있는 ViewModelProvider를 어떻게 생성하고 사용하는지 알아보았습니다.

 

하지만 이러한 과정을 거치는 것은 불편하기에 Android에서는 편하게 사용할 수 있는 by viewModels()` 확장함수를 제공합니다.


위에서 말했듯이 우리는 View가 구현하고 있는 ViewModelStore에 저장된 ViewModel의 인스턴스를 가져와야 합니다. 이 메서드는 ComponentActivity의 확장 함수로 설명했던 과정들을 수행해 줍니다.

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

 

 

리턴해주는 `ViewModelLazy`는 아래와 같습니다. 간단하게만 봐도 ViewModelStore를 받아서 이를 이용해 ViewModelProvider의 인스턴스를 생성하고 이를 통해 ViewModel의 인스턴스를 가져오고 있다는 것을 알 수 있습니다.

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
}

 

 

 

마무리하며


이렇게 해서 이번 게시글에서는 ViewModel의 객체를 어떻게 가져오는지 알아보았습니다. 다음 게시글에서는 Hilt와 함께  ViewModel의 객체를 가져오는 방법에 대해서 알아보고자 합니다.

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!