깨알 개념/Android

[Android] ViewModel - 뷰 모델에 인자(Argument) 전달 (ViewModelFactory)

interfacer_han 2024. 1. 15. 16:24

#1 ViewModelProvider 클래스 분석

#1-1 수정할 샘플 앱

 

[Android] View Model - 기초

#1 View Model의 필요성#1-1 예제버튼을 누르면 TextView의 text가 1씩 증가하는 예시 앱이다. MainActivity.kt 코드는 다음과 같다. // package com.example.viewmodelbasics import androidx.appcompat.app.AppCompatActivity import andro

kenel.tistory.com

위 게시글의 '완성된 앱'을 수정해서, ViewModel 클래스가 인자(Argument)를 받게 만들어본다.

 

그냥 class sampleViewModel(args: Int) : ViewModel() { ... }와 같이 인자를 받는 ViewModel 클래스를 만들어 쓰면 그만 아니냐는 생각도 든다. 하지만 이전 글에 따르면 View Model의 인스턴스는 View Model의 자체 생성자가 아니라, ViewModelProvier의 생성자를 이용한 간접적인 방법으로 생성된다. 먼저, 그 방법이라는 것을 자세히 살펴본다.

 

#1-2 ViewModelProvider의 생성자 살펴보기

...

public open class ViewModelProvider

/**
 * Creates a ViewModelProvider
 * Params:
 *     store - `ViewModelStore` where ViewModels will be stored.
 *     factory - factory a `Factory` which will be used to instantiate new `ViewModels`
 *     defaultCreationExtras - extras to pass to a factory
 */
constructor(
    private val store: ViewModelStore,
    private val factory: Factory,
    private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) {
    ...

    /**
    * Creates ViewModelProvider. This will create ViewModels and retain them in a store of the given ViewModelStoreOwner.
    * This method will use the default factory if the owner implements HasDefaultViewModelProviderFactory. Otherwise, a NewInstanceFactory will be used.
    */
    public constructor(
        owner: ViewModelStoreOwner
    ) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

    /**
    * Creates ViewModelProvider, which will create ViewModels via the given Factory and retain them in a store of the given ViewModelStoreOwner. 
    * Params: 
    *     owner - a ViewModelStoreOwner whose ViewModelStore will be used to retain ViewModels
    *     factory - a Factory which will be used to instantiate new ViewModels
    */
    public constructor(owner: ViewModelStoreOwner, factory: Factory) : this(
        owner.viewModelStore,
        factory,
        defaultCreationExtras(owner)
    )

    ...

    public interface Factory {
        ...
    }

    public companion object {
        internal fun defaultFactory(owner: ViewModelStoreOwner): Factory = ...
        
        ...
    }
}

internal fun defaultCreationExtras(owner: ViewModelStoreOwner): CreationExtras {
    ...
}

...

총 3개의 생성자가 보인다. 맨 위에 기본 생성자가 있다. 기본 생성자가 요구하는 인자는 3가지로 첫째는 ViewModelStore, 둘째는 ViewModelProvider 클래스 내부에 정의된 인터페이스인 ViewModelProvider.Factory, 셋째는 CreationExtras이다.

 

기본 생성자 뒤에, 보조 생성자인 2번째 생성자, 3번째 생성자도 있다. '2번째 생성자'가 이전 글에서 사용했던 생성자다. '2번째 생성자'는 ViewModelStore만 인자로 받고 나머지는 기본값(각각 함수 defaultFactory(), defaultCreationExtras())으로 처리한다. '3번째 생성자'는 ViewModelStore와 ViewModelProvider.Factory를 인자로 받고 CreationExtras만 기본값으로 처리한다.

 

어느 생성자든 간에 ViewModelProvider.Factory 클래스가 나온다. ViewModelProvider.Factory는 팩토리 메소드 패턴(Factory Method Pattern)을 구현하기 위한 클래스다. 즉, ViewModelProvider는 팩토리 메소드 패턴으로 ViewModel 클래스의 인스턴스를 생성하고 있다.

 

따라서, defaultFactory() 대신 내가 직접 ViewModelProvider.Factory의 구현 클래스를 만들 수 있다면, 이전 글의 코드를 '3번째 생성자'를 이용해 다시 짤 수 있다.

 

#2 '2번째 생성자' 대신 '3번째 생성자' 사용하기

#2-1 MainActivityViewModelFactory.kt 만들기

// package com.example.argumenttoviewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivityViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) {
            return MainActivityViewModel() as T
        }
        throw IllegalArgumentException("Unknown View Model Class")
    }
}

defaultFactory를 대신할 custom(사용자 정의) 클래스를 만든다. 이 클래스는 인터페이스 ViewModelProvider.Factory의 구현 클래스다. create()를 override하고 위와 같이 코드를 짠다. 이는 거의 모든 ViewModelProvider.Factory의 구현 클래스에서 사용하는 표준과도 같은 상용구 코드라고 한다. 프로그래머가 '2번째 생성자'를 쓰는 경우, ViewModelProvider는 이 구현 클래스와 같은 기능을 하는 defaultFactory()를 알아서 만들어 제공했던 것이다.

 

#2-2 MainActivity.kt 수정

...

class MainActivity : AppCompatActivity() {

    ...
    
    private lateinit var viewModelFactory: MainActivityViewModelFactory
    
    ...
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
        ...
        
        viewModelFactory = MainActivityViewModelFactory()
        viewModel = ViewModelProvider(this, viewModelFactory).get(MainActivityViewModel::class.java) // '3번째 생성자' 사용
        
        ...
    }
}

MainActivity.kt를 수정해, MainActivityViewModelFactory의 인스턴스를 만들고 '2번째 생성자' 대신 '3번자 생성자'를 사용해 ViewModel의 인스턴스를 만든다. 나머지 코드는 유지한다.

 

#2-3 구동 테스트

이전과 똑같이 잘 작동한다.

 

#3 ViewModel 객체에 인자 전달하기

사실 이미 #2에서 답이 나왔다. MainActivityViewModelFactory 클래스가 인수를 받도록 수정하면 끝이다. 일각에선 인수를 전달할 때 커스텀 팩토리를 만든다라고 하는데, 이는 엄밀하게는 논리적으로 틀린 말이다. #2에서 인수가 없어도 커스텀 팩토리로 만들었음을 보이지 않았는가. 따라서, 말을 깐깐하게 고치면 다음과 같다. ViewModel에 인수를 전달하지 않을거라면 '굳이' 커스텀 팩토리를 만들 '필요'가 없다.

ViewModel에 인수를 전달하는 코드는 다음과 같다.

 

#3-1 MainActivityViewModel.kt 수정

...

class MainActivityViewModel(startingCount : Int) : ViewModel() {
    private var count = 0

    init {
        count = startingCount
    }

    ...
}

init { ... } 블록을 쓰지 않고, private var count = startingCount 와 같이 코드를 짜도 된다. 혹은, init { ... } 블록과 private var count = 0 둘다 없애버리고, class 선언부에 class MainActivityViewModel(private var count : Int) : ViewModel()과 같이 적어도 된다.

 

#3-2 MainActivityViewModelFactory.kt 수정

...

class MainActivityViewModelFactory(private val startingCount: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) {
            return MainActivityViewModel(startingCount) as T
        }
        throw IllegalArgumentException("Unknown View Model Class")
    }
}

 

#3-3 MainActivity.kt 수정

...

class MainActivity : AppCompatActivity() {

    ...
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        viewModelFactory = MainActivityViewModelFactory(777)
        
        ...
    }
}

MainActivityViewModel.count에 777을 전달한다.

 

#3-3 구동 테스트

 

#4 요약

ViewModel의 생성자가 특이한 형태인 이유는 팩토리 메소드 패턴을 구현하기 위해서다.

 

#5 완성된 앱

https://github.com/Kanmanemone/android-practice/tree/master/view-model/ArgumentToViewModel