2
0

More than 3 years have passed since last update.

Dagger2使用時に引数ありのViewModelをinterface化する

Posted at

はじめに

本記事はDagger2 + MVVM構成のアーキテクチャで、引数ありのViewModelをinterface化する方法の備忘録です。

リポジトリ

以下に今回のサンプルを置いてあります。
https://github.com/nanaten/Dagger-ViewModel-Interface

プロジェクト構成

昨今のAndroid開発における標準的なMVVMを想定しています。

View (Fragment) -> ViewModel -> Repository ( -> API )

注意書き

今回、基本的なDagger2の構成の解説は省略します。 今回はViewModelに関わる部分のみ解説します。

※Dagger2の基本構成は【DI】Dagger2+Retrofit2(+OkHttp3)+ViewModelのDIの最小構成で解説していますのでそちらを参照してください。今回のプロジェクト構成も上記記事とほぼ変わりません。

解説

ViewModelは以下のような作りになっています。
MainViewModelImpl はコンストラクタに MainRepository の引数を持ちます。

MainViewModel
interface MainViewModel {
    ...
}

class MainViewModelImpl @Inject constructor(private val repository: MainRepository) : ViewModel(), MainViewModel {
    ...
}

コンストラクタ付きのViewModelを使うために ViewModelKeyViewModelFactoryを定義します。 ViewModelFactory についてはこちらの記事を参考にさせて頂いています。

ViewModelKey
@MustBeDocumented
@Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
ViewModelFactory
class ViewModelFactory @Inject constructor(
    private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                    break
                }
            }
        }
        if (creator == null) {
            throw IllegalArgumentException("unknown model class " + modelClass)
        }
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

作成したViewModelKeyを使って、ViewModelを以下のようにbindします。

@Binds
@IntoMap
@ViewModelKey(MainViewModelImpl::class)
fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel

bindしたModuleをinjectします。今回はFragmentで使いたかったのでAppComponentに書かずにActivityにmoduleを追加しました。

@Module
interface MainActivityModule {
    @Binds
    @IntoMap
    @ViewModelKey(MainViewModelImpl::class)
    fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel

    @ContributesAndroidInjector
    fun bindMainFragment(): MainFragment
}
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun bindMainActivity(): MainActivity

そして、FragmentでViewModelProviderを使って以下のように宣言すれば、interface化したViewModelを扱えるようになります。

MainFragment
class MainFragment : DaggerFragment() {
    @Inject
    lateinit var viewModelFactory: ViewModelFactory
    // MainViewModelImpl ではなく MainViewModel として型宣言する
    private val viewModel: MainViewModel by lazy {
        // 実態は MainViewModelImpl を生成する
        ViewModelProvider(this, viewModelFactory).get(MainViewModelImpl::class.java)
    }

}

型宣言をちゃんとしないと MainViewModelImpl として認識されてしまうのでご注意ください。

ちなみにActivityでViewModelを使う場合もほぼ同じです。Activityで使う場合はModuleをAppComponentに追加してください。

おわりに

ViewModelProvider を使うことに思い至らずに by viewModels を使おうとして詰まってました…

本記事を書くにあたって、以下のリンク先を参考にさせて頂きました。
[Qiita] ViewModelにLiveDataとMutableLiveDataを同時に宣言しなくていいようにする
[Qiita] Architecture Components を Dagger2 と併用する際の ViewModelProvider.Factory について

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0