ViewModelをDagger2でDIする方法について書きます。
自分も理解するまでかなり苦労したので誰かの力になればと思います。
正直自分の完全に理解したわけではないので、ここが違うよ!!とかあれば優しく教えていただれば嬉しいです。😭
ViewModelをDagger2を使ってDIする方法がまじでわからないので、一旦整理しよう。なにがわかっててなにがわからないのか、1からやり直すぞ
— shogo.yamada@Flutterが好き (@yshogo87) 2018年10月23日
ソースコードは下記に置いておきました。
準備
ライブラリを導入します。
def daggerVersion = "2.16"
implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
基本形
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: MainActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ↓ここをDagger2を使ってDIしたい
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.apply {
viewModel = this@MainActivity.viewModel
}
}
}
最終的にしたいコード
最終的にやりたいことはこんな感じだと思います。
よくみるサンプルプロジェクトですね。
// DaggerAppCompatActivityを継承させたい
class MainActivity : DaggerAppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: MainActivityBinding
// ViewModelFactoryをDIしたい
@Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ↓こういう風に書きたい
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.apply {
viewModel = this@MainActivity.viewModel
}
}
}
ViewModelFactoryを自作しなければいけないパターンはApplicationクラス以外をコンストラクタの引数としたい場合はDefaultFactoryではない、独自のViewModelProvider.Factoryを用意する必要があるようです。このときにDaggerを使ってDIします。
詳しくは下記の記事にかかれています。
MainModuleを作る
@Module
internal abstract class MainModule {
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}
-
@Binds
の説明 (https://qiita.com/sakuna63/items/1b3e755d6f26908aa8c9) -
@IntroMap
の説明 (https://qiita.com/m-dove/items/767c4bfaeee53caefc4d)
この実装をこのままコピペすると @ViewModelKey
がエラーになると思いますのでアノテーションを定義します。
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
これでViewModelをDIするためのコードをDaggerに生成してもらいます。
ActivityModuleを作る
次にActivityに対してどんなModuleをInjectするのか定義します。
@Module
abstract class ActivityModule {
@ActivityScoped
@ContributesAndroidInjector(modules = [
MainModule::class]
)
internal abstract fun mainActivity(): MainActivity
}
今回は MainActivity
に対して MainModule
をInjectするのでその定義をします。
ViewModelModuleを作る
ActivityModule
と同様、ViewModelModuleを作成します。
@Module
abstract class ViewModelModule {
@Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory):
ViewModelProvider.Factory
}
ViewModelFactoryを作る
ここまでは ViewModelModule
でコンパイルエラーが発生すると思うのでここでViewModelFactoryを作成します。
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>,
@JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<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 {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
この辺はとりあえずコピペしておけと言いたいのですが、そうはいきません。
さっきも登場した記事ですが、詳しくは下記にかかれていますので読んでください
AppComponentを作る
次にDaggerの作法なんですが、 @Component
が付いているインターフェースを実装します。
@Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
ActivityModule::class,
ViewModelModule::class
]
)
interface AppComponent : AndroidInjector<MainApplication> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<MainApplication>()
}
このインターフェースを作成することによって、 DaggerAppComponet
クラスがDaggerによって作られます。
DaggerApplicationを継承したApplicationクラスを設定する
MainApplication
を作成して DaggerApplication
クラスを継承します。
class MainApplication : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}
}
AndroidManifest.xml
に書くことを忘れずに。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.shogoyamada.dagger2_sample">
<application
android:name=".MainApplication" // ←ここを忘れずに
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ViewModelのコンストラクタに @Inject constructor
をつける
あとはViewModelに対して @Inject constructor
をつけます。
class MainViewModel @Inject constructor(useCase: MainUseCase) : ViewModel() {
var test = useCase.getGreeting()
}
よくあるのはここのコンストラクタでAPI叩くクラスをインジェクトしたり、ローカルDBにアクセスするクラスをインジェクトしたりするかと思います。
これは最初に書いたように
ViewModelFactoryを自作しなければいけないパターンはApplicationクラス以外をコンストラクタの引数としたい場合はDefaultFactoryではない、独自のViewModelProvider.Factoryを用意する必要があるようです。このときにDaggerを使ってDIします。
が実現できています。
ちなみにGoogle I/O 2018のコードもそういうようなことをしています。
class MapViewModel @Inject constructor(
loadMapTileProviderUseCase: LoadMapTileProviderUseCase,
private val loadGeoJsonFeaturesUseCase: LoadGeoJsonFeaturesUseCase,
private val analyticsHelper: AnalyticsHelper
) : ViewModel() {
// hogehogeの実装
}
完成
あとは最初に「こう書きたい!!」って感じで書いていたActivityを書けば完成です。
class MainActivity : DaggerAppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: MainActivityBinding
@Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.apply {
viewModel = this@MainActivity.viewModel
}
}
}
所感
「Daggerはわかりづらい!!」ってよく言われますが、実際やってみると本当にわかりづらい。最初勉強し始めた時にソースコードが追えなさすぎてわけわからなくなりました。
まだ触ってませんが、Koinとかもいいかもしれません。
参考
https://github.com/InsertKoinIO/koin
https://qiita.com/sudachi808/items/8e03503f52b4f11533a2
ここまで書いておいてちゃんと理解できているわけではないのでもし、なんか指摘があれば優しく教えていただけると嬉しいです。