SavedStateHandle을 다루는 ViewModel을 위한 Dagger 설정하기

Warning : 이 포스팅은 ViewModel과 Dagger에 대한 내용을 다루고 있지만, ViewModel과 Dagger에 대해서는 설명하고 있지 않습니다.  자세한 내용은 이전 포스팅을 참고해주세요.

예제코드 다운로드

build.gradle에 의존성 설정하기

//SavedState
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"

//Dagger2
implementation 'com.google.dagger:dagger:2.27'
implementation 'com.google.dagger:dagger-android:2.27'
implementation 'com.google.dagger:dagger-android-support:2.27'
kapt 'com.google.dagger:dagger-compiler:2.27'
kapt 'com.google.dagger:dagger-android-processor:2.27'

//AssistedInject
compileOnly "com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2"
kapt "com.squareup.inject:assisted-inject-processor-dagger2:0.5.2"

각 라이브러리의 릴리즈 노트를 통해 최신버전을 확인하자 ( SavedState, Dagger2, AssistedInject )

AssitedInject를 위한 Base Factory 만들기

/**
 * SavedStateHandle을 포함하는 ViewModel을 생성하기 위한 범용적인 Factory
 * 이 Factory는 하나의 InjectingSavedStateViewModelFactory에 모든 ViewModel을 가질 수 있도록 한다.
 */
interface AssistedSavedStateViewModelFactory<T : ViewModel> {
    fun create(savedStateHandle: SavedStateHandle): T
}

ViewModel을 확장한 제너릭 T를 반환하는 create 메서드를 만든다. savedStateHandle은 Dagger 그래프에 포함할 수 없는 동적인 파라미터로 create의 매개변수로 받는다. ViewModel 타입을 제너릭으로 받는 이유는 이어서 나올 Dagger의 @Multibinds Map으로 모든 ViewModel의 Factory를 관리하기 위함이다.

ViewModel에 AssistedInject를 위한 코드 추가하기

class MainViewModel @AssistedInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    ...
    @AssistedInject.Factory
    interface Factory : AssistedSavedStateViewModelFactory<MainViewModel>

}

AssistedInject를 통해 SavedStateHandle을 생성자 매개변수로 받을 수 있도록 설정한다. Factory의 경우 AssistedSavedStateViewModelFactory를 상속하여 MainViewModel을 반환하는 create() 메서드가 포함되도록 한다. 이렇게 설정하고 나면 AssistedInject 라이브러리는 컴파일 타임에 MainViewModel.Factory를 구현한 MainViewModel_AssistedFactory 클래스를 생성할 것이다.

ViewModel들을 멀티바인딩으로 관리 하기

@AssistedModule
@Module(includes = [AssistedInject_ViewModelModule::class])
abstract class ViewModelModule{

    //일반 뷰모델들의 멀티 바인딩
    @Multibinds
    abstract fun bindsViewModels(): Map<Class<out ViewModel>, @JvmSuppressWildcards ViewModel>

    //AssistedInject로 관리하는 ViewModel Factory 멀티바인딩
    @Multibinds
    abstract fun bindsAssistedViewModels(): Map<Class<out ViewModel>, @JvmSuppressWildcards AssistedSavedStateViewModelFactory<out ViewModel>>

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel::class)
    abstract fun bindsMainViewModel(factory: MainViewModel.Factory): AssistedSavedStateViewModelFactory<out ViewModel>

}

@AssistedModule이 추가된 ViewModelModule을 하나 만든다. 이 모듈은 AssistedInject를 통해 대거 그래프에 추가되는 AssistedSavedStateViewModelFactory들을 Map으로 관리하는 모듈 클래스다. MainViewModel_AssistedFactory도 AssistedSavedStateViewModelFactory의 서브클래스 이므로 bindsMainViewModel 바인딩 메서드를 통해 Map으로 관리되는 것을 확인할 수 있다.

AbstractSavedStateViewModelFactory 만들기

/**
 * ViewModel들을 인스턴스화 하기 위해 이 클래스를 사용할 수 있다.
 * Fragment/Activity 에서 이 Factory를 주입받아 ViewModel을 생성하는데 사용할 수 있다.
 */
@Singleton
class InjectingSavedStateViewModelFactory
@Inject constructor(
    private val assistedFactories: Map<Class<out ViewModel>, @JvmSuppressWildcards AssistedSavedStateViewModelFactory<out ViewModel>>,
    private val viewModelProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) {
    /**
     * @AssistedInject 또는 @Inject로 어노테이션이 달린 ViewModel 인스턴스를 작성하고 필요한 종속성을 전달한다.
     */
    fun create(owner: SavedStateRegistryOwner, defaultArgs: Bundle? = null) =
        object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
            override fun <T : ViewModel?> create(
                key: String,
                modelClass: Class<T>,
                handle: SavedStateHandle
            ): T {
                val viewModel =
                    createAssistedInjectViewModel(modelClass, handle)
                        ?: createInjectViewModel(modelClass)
                        ?: throw IllegalArgumentException("Unknown model class $modelClass")

                try {
                    @Suppress("UNCHECKED_CAST")
                    return viewModel as T
                } catch (e: Exception) {
                    throw RuntimeException(e)
                }
            }
        }

    /**
     * @AssistedInject 생성자와 해당 Factory를 기반으로 ViewModel을 생성한다.
     */
    private fun <T : ViewModel?> createAssistedInjectViewModel(
        modelClass: Class<T>,
        handle: SavedStateHandle
    ): ViewModel? {
        val creator = assistedFactories[modelClass]
            ?: assistedFactories.asIterable()
                .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
            ?: return null

        return creator.create(handle)
    }

    /**
     * 생성자에 @Inject가 있는 일반적인 Dagger 기반의 ViewModel을 생성한다.
     */
    private fun <T : ViewModel?> createInjectViewModel(
        modelClass: Class<T>
    ): ViewModel? {
        val creator = viewModelProviders[modelClass]
            ?: viewModelProviders.asIterable()
                .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
            ?: return null

        return creator.get()
    }
}

InjectingSavedStateViewModelFactory는 AbstractSavedStateViewModelFactory를 반환하는 create() 메서드를 가지고 있다. InjectingSavedStateViewModelFactory의 생성자 인젝션을 통해 assistedFactories라는 변수명을 갖는 멀티바인딩 Map을 주입받게 되는데, 이 멀티바인딩 Map을 통해 특정 ViewModel의 Factory를 얻어 ViewModel 인스턴스를 얻고, 그것을 통해 AbstractSavedStateViewModelFactory를 만들게 된다. Factory라는 명칭이 중첩되어 헷갈릴 수 있으니 주의해야 한다. ViewModel에 있는 Factory는 AssistedInject용 Factory이며, AbstractSavedStateViewModelFactory는 ViewModelProvider.Factory임을 잊지 말자. (Factory로 Factory를 만든다니…)

액티비티에서 ViewModel 만들기

@Module
class MainModule{

    @Provides
    @ActivityScope
    fun provideViewModelProvider(activity:MainActivity, viewModelFactory:InjectingSavedStateViewModelFactory):ViewModelProvider{
        return ViewModelProvider(activity, viewModelFactory.create(activity))
    }

}
class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var viewModelProvider: ViewModelProvider
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = viewModelProvider.get(MainViewModel::class.java)
        ...
    }
    ...
}

MainModule은 MainActivitySubcomponent에 추가된 모듈이다. 달리 말하면 ActivityScope내에서 의존성을 주입하는 모듈이다.

MainModule에서는 ViewModelProvider를 반환하는 프로바이드 메서드를 가지고 있다. 이 provideViewModelProvider 메서드는 대거 그래프에 있는 MainActivity와 InjectingSavedStateViewModel 인스턴스를 메서드 매개변수를 통해 제공받는다.

ViewModelProvider를 반환해야하므로 해당 생성자를 통해 ViewModelProvider를 만들게 되는데, 첫번째 인자는 ViewModelStoreOwner이므로 MainViewModel을 저장하고 관리할 MainActivity를 ViewModelStoreOwner로 지정한다.

ViewModelProvider의 생성자 두번째 인자는 ViewModelProvider.Factory인데, ViewModel에서 SavedStateHandle 다뤄야하므로 반드시 AbstractSavedStateViewModelFactory를 ViewModelProvider 생성자의 두번째 인자로 지정해야한다.

프로바이더 메서드의 매개변수인 InjectingSavedStateViewModel 인스턴스를 통해 AbstractSavedStateViewModelFactory 인스턴스를 얻는 과정을 살펴보자.

InjectingSavedStateViewModel의 create 메서드는 AbstractSavedStateViewModelFactory를 반환한다. create 메서드의 매개변수로 저장된 상태 인스턴스(savedInstanceState) 보관하는 SavedStateRegistryOwner를 받고 있는 것을 확인할 수 있다. MainActivity는 ComponentActivity의 서브클래스이므로 SavedStateRegistryOwner를 이미 구현하고 있다. 그렇기 때문에 create메서드의 매개변수로 MainActivity를 지정할 수 있다. 

모든 과정이 끝나면 대거 그래프에는 ViewModelProvider가 바인딩 된다.

MainActivity에서는 MainActivitySubcomponent로 부터 ViewModelProvider를 제공받아 MainViewModel을 인스턴스화 하고 있다.

Conclusion

이 포스팅을 통해 Dagger와 함께 동작하는 SavedStateHandle을 갖는 ViewModel을 설명했다. 내용을 쉽게 풀어서 설명하려 했으나 망한것 같다. github에 있는 예제 코드를 통해 통찰을 얻으시길 바란다.

 

카테고리: Dagger2

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.