はじめに
本記事は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
の引数を持ちます。
interface MainViewModel {
...
}
class MainViewModelImpl @Inject constructor(private val repository: MainRepository) : ViewModel(), MainViewModel {
...
}
コンストラクタ付きのViewModelを使うために ViewModelKey
と ViewModelFactory
を定義します。 ViewModelFactory
についてはこちらの記事を参考にさせて頂いています。
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
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を扱えるようになります。
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 について