LoginSignup
30
23

More than 5 years have passed since last update.

ViewModelをDagger2でDIする

Posted at

ViewModelをDagger2でDIする方法について書きます。
自分も理解するまでかなり苦労したので誰かの力になればと思います。

正直自分の完全に理解したわけではないので、ここが違うよ!!とかあれば優しく教えていただれば嬉しいです。😭

ソースコードは下記に置いておきました。

準備

ライブラリを導入します。

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
}

この実装をこのままコピペすると @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

ここまで書いておいてちゃんと理解できているわけではないのでもし、なんか指摘があれば優しく教えていただけると嬉しいです。

30
23
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
30
23