Hanbit the Developer

ViewModelStore, ViewModelProvider 분석 본문

Mobile/Android

ViewModelStore, ViewModelProvider 분석

hanbikan 2024. 1. 12. 22:54

ViewModel 생성 방법

ViewModelStore, ViewModelProvider에 대해 알아보기 전에, ViewModel을 어떻게 생성할 수 있는지부터 파악하여 top-down으로 알아보고자 한다.

ViewModelProvider를 통한 생성

ViewModelProvider라는 클래스를 생성하고 get 함수를 통해 뷰모델을 얻을 수 있다.

이때 ViewModelProvider.Factory라고 하는, 뷰모델 생성 방식을 정의한 인터페이스를 따로 넣어줄 수도 있다. 지정하지 않으면 디폴트 팩토리를 사용하게 된다.

class MyActivity : AppCompatActivity() {
 
    private lateinit var myViewModel: MyViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
				// ...

        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        // or
        // myViewModel = ViewModelProvider(this, MyViewModel.Factory).get(MyViewModel::class.java)
    }

    // ...
}

viewModels()를 통한 생성

viewModels() 함수에 delegate하여 생성할 수도 있다.

class MyActivity : AppCompatActivity() {

    private val viewModel: MyViewModel by viewModels()
    // or
    // private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }

    // ...
}

viewModels()의 구현은 ViewModelLazy 클래스를 반환하는 식으로 되어 있다.

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

ViewModelLazy는 Lazy를 구현하고 있으며, [ViewModelProvider를 통한 생성]에서 본 방식과 같이 ViewModelProvider를 생성하고 get을 통해 뷰모델을 반환하고 있다.

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
}

Compose: viewModel()을 통한 생성

위의 경우와는 달리 ViewModelStoreOwner.get()을 통해 뷰모델을 반환하고 있다.

@Suppress("MissingJvmstatic")
@Composable
public fun <VM : ViewModel> viewModel(
    modelClass: Class<VM>,
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
        viewModelStoreOwner.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM = viewModelStoreOwner.get(modelClass, key, factory, extras)

하지만 해당 함수의 내부 구현을 보니 역시 ViewModelProvider를 통해 뷰모델을 반환하고 있다.

private fun <VM : ViewModel> ViewModelStoreOwner.get(
    javaClass: Class<VM>,
    key: String? = null,
    factory: ViewModelProvider.Factory? = null,
    extras: CreationExtras = if (this is HasDefaultViewModelProviderFactory) {
        this.defaultViewModelCreationExtras
    } else {
        CreationExtras.Empty
    }
): VM {
    val provider = if (factory != null) {
        ViewModelProvider(this.viewModelStore, factory, extras)
    } else if (this is HasDefaultViewModelProviderFactory) {
        ViewModelProvider(this.viewModelStore, this.defaultViewModelProviderFactory, extras)
    } else {
        ViewModelProvider(this)
    }
    return if (key != null) {
        provider[key, javaClass]
    } else {
        provider[javaClass]
    }
}

Compose: hiltViewModel()을 통한 생성

createHiltViewModelFactory()로 ViewModelFactory를 얻어 viewModel()를 호출하는 방식이다.

@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}

@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
    HiltViewModelFactory(
        context = LocalContext.current,
        delegateFactory = viewModelStoreOwner.defaultViewModelProviderFactory
    )
} else {
    // Use the default factory provided by the ViewModelStoreOwner
    // and assume it is an @AndroidEntryPoint annotated fragment or activity
    null
}

뷰모델 생성 코드들을 보면 아래 클래스들이 주요 요소임을 알 수가 있다:

  • ViewModelStoreOwner
  • ViewModelStore
  • ViewModelProvider
  • CreationExtras

ViewModelStoreOwner

말 그대로 ViewModelStore를 멤버 변수로 들고 있는 인터페이스이다. 추가로 스코프가 destroy 될 때 ViewModelStore.clear() 함수를 호출하는 책임을 진다.

이 인터페이스를 구현하는 대표적인 예시는 액티비티와 프래그먼트이다.

/**
 * A scope that owns [ViewModelStore].
 *
 * A responsibility of an implementation of this interface is to retain owned ViewModelStore
 * during the configuration changes and call [ViewModelStore.clear], when this scope is
 * going to be destroyed.
 *
 * @see ViewTreeViewModelStoreOwner
 */
interface ViewModelStoreOwner {

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

ViewModelStore

그럼 ViewModelStore가 무엇인가? 주석을 간략히 풀면 다음과 같다:

ViewModel을 여러 개 저장해두는 클래스로, configuration change(액티비티 회전, 언어 변경 등)으로 ViewModelStore의 오너(액티비티, 프래그먼트)가 제거되고 다시 생성되더라도 ViewModelStore 인스턴스는 같은 인스턴스로 유지되어야 한다. 만약 ViewModelStore의 오너가 제거되고 다시 생성되지 않으면 clear 함수를 호출하여 안에 있는 뷰모델들을 더이상 쓰지 않는다고 알려야 한다.

구현은 간단하다. Map<String, ViewModel)에 뷰모델을 여러 개 저장하여 관리하고 있으며, clear 함수가 호출되면 내부 뷰모델을 돌며 ViewModel.clear()를 호출한다.

/**
 * Class to store `ViewModel`s.
 *
 * An instance of `ViewModelStore` must be retained through configuration changes:
 * if an owner of this `ViewModelStore` is destroyed and recreated due to configuration
 * changes, new instance of an owner should still have the same old instance of
 * `ViewModelStore`.
 *
 * If an owner of this `ViewModelStore` is destroyed and is not going to be recreated,
 * then it should call [clear] on this `ViewModelStore`, so `ViewModel`s would
 * be notified that they are no longer used.
 *
 * Use [ViewModelStoreOwner.getViewModelStore] to retrieve a `ViewModelStore` for
 * activities and fragments.
 */
open class ViewModelStore {

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

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.onCleared()
    }

    /**
     * Returns the `ViewModel` mapped to the given `key` or null if none exists.
     */
    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    operator fun get(key: String): ViewModel? {
        return map[key]
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    /**
     * Clears internal storage and notifies `ViewModel`s that they are no longer used.
     */
    fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

ViewModelProvider

ViewModelProvider는 뷰모델을 제공하는 유틸리티 클래스이다. 코드는 다음과 같이 나눌 수 있다:

  • (interface) ViewModelProvider.Factory
  • constructor
  • get 함수
  • class NewInstanceFactory, AndroidViewModelFactory: 기본 제공되는 Factory 구현체

이 중 가장 중요한 Factory, get 함수에 대해 다루고자 한다.

get 함수

외부에서 get() 함수로 MyViewModel을 요청하는 상황을 가정해보자. 멤버 변수 store(ViewModelStore)에 MyViewModel이 있으면 그것을 반환하고, 없으면 factory(ViewModelProvider.Factory)로 뷰모델을 생성해서 반환한다.

/**
 * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
 * an activity), associated with this `ViewModelProvider`.
 *
 * The created ViewModel is associated with the given scope and will be retained
 * as long as the scope is alive (e.g. if it is an activity, until it is
 * finished or process is killed).
 *
 * @param key        The key to use to identify the ViewModel.
 * @param modelClass The class of the ViewModel to create an instance of it if it is not
 * present.
 * @return A ViewModel that is an instance of the given type `T`.
 */
@Suppress("UNCHECKED_CAST")
@MainThread
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
    // AGP has some desugaring issues associated with compileOnly dependencies so we need to
    // fall back to the other create method to keep from crashing.
    return try {
        factory.create(modelClass, extras)
    } catch (e: AbstractMethodError) {
        factory.create(modelClass)
    }.also { store.put(key, it) }
}

ViewModelProvider.Factory

ViewModel 생성 함수 create를 정의하는 인터페이스다. 경험상 디폴트 팩토리 클래스를 사용하는 경우가 가장 많았고, 직접 팩토리 클래스를 만들어 사용할 수도 있다.

/**
 * A utility class that provides `ViewModels` for a scope.
 *
 * Default `ViewModelProvider` for an `Activity` or a `Fragment` can be obtained
 * by passing it to the constructor: `ViewModelProvider(myFragment)`
 */
public open class ViewModelProvider
/**
 * Creates a ViewModelProvider
 *
 * @param store `ViewModelStore` where ViewModels will be stored.
 * @param factory factory a `Factory` which will be used to instantiate new `ViewModels`
 * @param defaultCreationExtras extras to pass to a factory
 */
@JvmOverloads
constructor(
    private val store: ViewModelStore,
    private val factory: Factory,
    private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) {
    /**
     * Implementations of `Factory` interface are responsible to instantiate ViewModels.
     */
    public interface Factory {
        /**
         * Creates a new instance of the given `Class`.
         *
         * Default implementation throws [UnsupportedOperationException].
         *
         * @param modelClass a `Class` whose instance is requested
         * @return a newly created ViewModel
         */
        public fun <T : ViewModel> create(modelClass: Class<T>): T {
            throw UnsupportedOperationException(
                "Factory.create(String) is unsupported.  This Factory requires " +
                    "`CreationExtras` to be passed into `create` method."
            )
        }

        /**
         * Creates a new instance of the given `Class`.
         *
         * @param modelClass a `Class` whose instance is requested
         * @param extras an additional information for this creation request
         * @return a newly created ViewModel
         */
        public fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
            create(modelClass)

        companion object {
            /**
             * Creates an [InitializerViewModelFactory] using the given initializers.
             *
             * @param initializers the class initializer pairs used for the factory to create
             * simple view models
             */
            @JvmStatic
            fun from(vararg initializers: ViewModelInitializer<*>): Factory =
                InitializerViewModelFactory(*initializers)
        }
    }
		// ...
}

CreationExtras

CreationExtras는 ViewModelProvider, Factory에서 인자로 쓰이며 추가 정보를 제공한다.

/**
 * Simple map-like object that passed in [ViewModelProvider.Factory.create]
 * to provide an additional information to a factory.
 *
 * It allows making `Factory` implementations stateless, which makes an injection of factories
 * easier because  don't require all information be available at construction time.
 */
public abstract class CreationExtras internal constructor() {
    internal val map: MutableMap<Key<*>, Any?> = mutableMapOf()

    /**
     * Key for the elements of [CreationExtras]. [T] is a type of an element with this key.
     */
    public interface Key<T>

    /**
     * Returns an element associated with the given [key]
     */
    public abstract operator fun <T> get(key: Key<T>): T?

    /**
     * Empty [CreationExtras]
     */
    object Empty : CreationExtras() {
        override fun <T> get(key: Key<T>): T? = null
    }
}

코드 예시

CreationExtras를 활용하는 예시 코드이다. create 함수에서, 뷰모델 생성에 필요한 인자를 extras를 통해 가져와 사용하고 있다.

class MyViewModel(val userId: String, val repository: MyRepository) : ViewModel() {
    // ...
}
class MyViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            val userId = extras[USER_ID_KEY] as String
            val repository = extras[REPOSITORY_KEY] as MyRepository

            return MyViewModel(userId, repository) as T
        }
    }
}
val userId = "12345"
val repository = MyRepository()

val extras = CreationExtras.Builder()
    .put(USER_ID_KEY, userId)
    .put(REPOSITORY_KEY, repository)
    .build()

val viewModel = ViewModelProvider(this, MyViewModelFactory(), extras)[MyViewModel::class.java]

추가 궁금증

여기까지 뷰모델 관련 클래스들의 구현을 살펴보았다. 마지막으로 코드를 보면서 개인적으로 궁금했던 사항들을 다루고자 한다.

Question

ViewModelStore는 Activity나 Fragment가 제거되고 재생성되어도 같은 인스턴스로서 유지가 된다. 이를 누가 어떻게 관리해주는가?

Answer

이를 위해 ViewModelStoreOwner의 구현체인 Fragment.java 코드를 살펴보았다.

1. Fragment에서 FragmentManager를 통해 ViewModelStore를 가져온다.

// Fragment.java
@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (mFragmentManager == null) {
        throw new IllegalStateException("Can't access ViewModels from detached fragment");
    }
    if (getMinimumMaxLifecycleState() == Lifecycle.State.INITIALIZED.ordinal()) {
        throw new IllegalStateException("Calling getViewModelStore() before a Fragment "
                + "reaches onCreate() when using setMaxLifecycle(INITIALIZED) is not "
                + "supported");
    }
    return mFragmentManager.getViewModelStore(this);
}

2. FragmentManager는 FragmentManagerViewModel 클래스(mNonConfig)를 멤버 변수로 지니고 있고, 이 멤버 변수의 getViewModelStore() 함수를 호출하여 ViewModelStore를 가져온다.

// FragmentManager.java
@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
    return mNonConfig.getViewModelStore(f);
}

3. FragmentManagerViewModel에서 mViewModelStores라는 HashMap(key: Fragment.mWho 식별자, value: ViewModelStore)으로 ViewModelStore들을 관리하는 것을 확인할 수 있었다. 이 멤버 변수를 통해 ViewModelStore를 반환한다.

// FragmentManagerViewModel.java
@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
    ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
    if (viewModelStore == null) {
        viewModelStore = new ViewModelStore();
        mViewModelStores.put(f.mWho, viewModelStore);
    }
    return viewModelStore;
}

 

Reference

https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories

https://readystory.tistory.com/176

https://pluu.github.io/blog/android/2022/03/12/creationextras/

'Mobile > Android' 카테고리의 다른 글

Android Document | App startup time  (1) 2024.01.25
Room Migration 방법  (0) 2024.01.17
AnimatedVisibility 오버랩 이슈 해결  (1) 2024.01.11
ViewModel 분석  (0) 2023.12.04
Android Document | Foreground services > Overview  (2) 2023.11.21