11
10

More than 3 years have passed since last update.

Dagger2をいまさらだけど導入する2019

Last updated at Posted at 2019-11-13

自分の担当するプロジェクトで全然Daggerに触れる機会がないので、いい機会なのでDagger2を今更導入してみました。

きっかけ

自分が今取り組んでいるDDDにおいて、各レイヤーごとに処理を分ける必要がありますが、プレゼンテーション層から下のレイヤーのインスタンスを作るのが非常に手間だったからです。
DIを利用して依存性を注入してあげれば、ActivityやFragmentから

HomeActivity.kt
private lateinit var homeApplicationService: HomeApplicationService

override fun onCreate(bundle: Bundle?) {
    ・・・
    val homeDatabase = HomeDatabase(this)
    val homeCache = HomeCache(this)
    val homeDataStore = HomeDataStore(homeDatabase, homeCache)
    val homeRepository = HomeRepository(homeDataStore)
    homeApplicationService = HomeApplicationService(homeRepository)
    ・・・

みたいなことをしなくて済むからです。(現在のプロジェクトでは仕方なくこれをしてます)

Dagger2

まずはbuild.gradleのdependenciesにライブラリを指定します。

build.gradle
apply plugin: 'kotlin-kapt'

dependencies {
    ・・・
    // dagger
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:2.24"
}

Javaの場合はkapt使えないので、annotationProcesserで指定してあげる必要があるので注意です。

DIパターン

DIについては2パターンあると思ってます。
1. ActivityやFragmentのインスタンスをDIする。
2. 1.以外のモジュールのインスタンスをDIする。

1と2でやり方があるのですが、DIする場合は基本的にActivityやFragmentでインスタンスを作ることで、結合が疎になると思うので、まとめて覚えてしまうほうがいいかなと思います。(所感)

とりあえず

この記事を読んでもいいですが、すでDaggerについての記事はたくさんあるので、
以下を参照すれば大丈夫です。
ぼくは以下を参考に実装しました。
https://qiita.com/satorufujiwara/items/f42b176404287690f1d0
https://qiita.com/satorufujiwara/items/0f95ccfc3820d3ee1370
https://qiita.com/superman9387/items/bea94e4316c2ccf8fb68
https://qiita.com/kenji1203/items/0f221272620309ebc1e9

興味がある方は読み進めてみてください。

AppComponentを作る

まずはAppComponentを作ります。

Componentは基本的にAppComponentくらいしか使ってないケースが多いですが、もしかしたらマルチモジュールでの開発の場合などでは、AppComponentではなく○○Componentみたいなものを作り、必要があった場合に必要な場所でDagger○○ComponentをDIするという方法もあるかもしれません。(多分)
では、実装例です。

実装例

AppComponent.kt
@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    MainModule::class
])
interface AppComponent: AndroidInjector<App> {
    @Component.Factory
    abstract class Factory : AndroidInjector.Factory<App>
}

説明

インタフェースの作成

AppComponentInterfaceで定義します。
定義したら、@Componentのアノテーションを付与します。
@Component内にmodule = []を記述し、関連するModuleを指定してあげます。
ようするに、ここは各Moduleで設定しているインスタンスを作る窓口的な役割です。

ここでは
AppModuleAndroidSupportInjectionModule::class
の2つを@Componentmodulesに指定します。
AppModuleについては後述します。

継承元について

今回、AndroidInjector<T>を継承しています。
TにDIさせる場所を指定することでDIが出来ます。
今回はAppとしていて、独自アプリケーションクラスを指定していますが、MainActivityなど、
Activityを指定することも可能です。
その代わり、指定したクラスでのインジェクトが必要になるので、DIする場所に気をつけてください。

抽象クラスFactoryの定義

インタフェース内に、AndroidInjector.Factoryを継承したクラスを定義し、
@Component.Factoryアノテーションを使用しています。
他の記事にもありますが、@ComponentにはFactoryBuilderがあり、
そのクラスを定義してあげることでDIすることができるようになるので、
作成する必要があります。

@Component.Builderでの実装例が今までの記事では書かれていますが、
@Component.Builderは非推奨になったので、今回はFactoryを利用します。

Appクラスを作る

この例では独自ApplicationクラスをInjectするクラスにしていしています。
ですので、Appクラスを作成します。

class App : DaggerApplication() {
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.factory().create(this)
    }
}

普通にApplicationクラスを継承する方法もありますが、今回はDaggerApplication
継承元にします。
これにより、injectを行うメソッドをOverrideできますので、その中で生成されたDaggerAppComponentを呼び出します。

生成したあとにAndroidManifest.xmlに独自Applicationクラスを指定することを忘れないようにしてください。

    <application
        android:name=".App"  // これ
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"

ビルドする

AppComponentクラスを作りますが、これだけでは動きません。
ビルドをしてDaggerAppComponentを作りましょう。

Build -> Make Module で作成できます。
image.png

Moduleを作る

AppComponentができたので、次はModuleを作りましょう。
まずサンプルとしてAppModule.ktというもの作ります。ここでは仮のRepositoryクラスをDIさせます。

HomeRepositoryImpl.kt
class HomeRepositoryImpl
    @Inject constructor(
        private val application: App
    ): HomeRepository {
    //    override fun get() = "daggerの確認だよ"
    @RequiresApi(Build.VERSION_CODES.N)
    override fun get(): String {
        return application.isDeviceProtectedStorage.toString()
    }
}
AppModule.kt
@Module
class AppModule {
    @Singleton
    @Provides
    fun providesHomeRepository(homeRepositoryImpl: HomeRepositoryImpl): HomeRepository
            = homeRepositoryImpl
}

今回は@Providesを使用しました。
基本的なお作法としてつけるメソッド名にはアノテーションに合わせるようです。(必須ではないハズ・・)

@Provides -> providesHomeRepository()
@Binds -> bindHomeRepository()
@ContributesAndroidInjector -> contributeHomeFragment()

指定する戻り値は、DIしたいインスタンスです。

Bindパターン

Moduleは@Bindでも行えます。
@Bindの場合は以下です。これは@Providesしているのと同義です。

@Module
abstract class AppModule {
    @Singleton
    @Binds
    abstract fun bindHomeRepository(homeRepository: HomeRepository): HomeRepository
}

@Bindsの方がすっきりしますね。
@Bindsを利用する場合、同Module内に@Providesを指定することが出来ないので、お気をつけください。

AppComponentに追加する

作成したModuleは、Componentに紐づけるかサブコンポーネントに紐付けるかしてあげないとDIされません。
先述していますが、AppComponentの@Component内にmoduleがあるので、そこで作成したModuleを紐付けてあげましょう。
今回のAppComponentはApp.ktでInjectしているので、アプリ全体で利用するためのInjectです。

AppComponent.kt
@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    MainModule::class,
    AppModule::class // 追加する
])
interface AppComponent: AndroidInjector<App> {
    @Component.Factory
    abstract class Factory : AndroidInjector.Factory<App>
}

ここまでやったら、あとはAppModuleでインスタンスを作ったものをインジェクトするだけです!

MainActivity.kt
class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var homeRepository: HomeRepository
}

これでOK!!!

ActivityをDIする

次にActivityをComponentに指定していきます。
正直、この記事を書くまでは以下の疑問点がありました。

  • ActivityをDIするってどういうこっちゃ
  • ActivityはそもそもstartActivityで起動なのでは?
  • とにかくよくわからん

とりあえず、参考にしていた記事を元に作ってみましょう。

ActivityModuleを作る

先程作っていたアプリではMainActivityがAndroidManifestでLAUNCHER指定していたので、
基本的にApplication -> MainActivityと呼び出されるので、DIする必要がないように思えます。

なので、別のHomeActivityというものを作ってDIしてみたいと思います。

HomeActivity.kt
class HomeActivity: DaggerAppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("HomeActivity::onCreate")

        val fragmentTransaction = supportFragmentManager.beginTransaction()
        val fragment = HomeFragment()
        fragmentTransaction.add(fragment, "homefragment")
    }
}

こんなやつのModuleを作ります。

HomeActivityModule.kt
@Suppress("unused")
@Module
abstract class HomeActivityModule {
    @ContributesAndroidInjector
    abstract fun contributeHomeActivity(): HomeActivity
}

これをAppComponentに指定します。

AppComponent.kt
@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    MainModule::class,
    AppModule::class,
    HomeActivityModule::class // 追加する
])
interface AppComponent: AndroidInjector<App> {
    @Component.Factory
    abstract class Factory : AndroidInjector.Factory<App>
}

これでActivityをComponent経由でDIできました(?)

検証

もしApplicationクラスでDIされてしまっていたらHomeActivityは謎にonCreateされてしまっていることでしょう。
先程のHomeActivity::onCreate()にprintlnを仕込んだのでログを確認してみます。

image.png

ログなし。

よくわからんので調べてみました。
https://qiita.com/MoyuruAizawa/items/26cb093adbc778013467

これに書いてある情報だと、ActivityModuleはActivityのライフサイクルに合わせて関連しているものをDIしてくれるそうです。

結論としては、Activityに関連するものをDIするために、事前にAppComponentなどで予約しておくような感じかなと思いました。
この場合、Activity単体で何かをやるためにというわけではなく、それに付随するFragmentやViewModel、さらに下のレイヤーのインスタンスをSubComponentを利用してDIするのが有効です。

ということで、次でFragmentとそれに付随するViewModel、さらにその下のRepositoryまでDIするようにします。

FragmentとViewModelをDIする

記事が長くなってきたのでだいぶ内容が荒くなってきていますが、最後にちゃちゃっとFragmentとViewModelをSubcomponentを用いてDIしたいと思います。

Subcomponentについては別で記事を書いて説明したいと思います。
イメージとしては
そのModuleに関連づくModuleを紐付けてDIしていきます。

では、Moduleです。

HomeModule.kt
@Module
abstract class HomeModule {
    @Binds
    @IntoMap
    @ViewModelKey(HomeViewModel::class)
    abstract fun bindHomeViewModel(homeViewModel: HomeViewModel): HomeViewModel

    @ContributesAndroidInjector
    abstract fun contributeHomeFragment(): HomeFragment
}

ここではHomeViewModelとHomeFragmentをDIしています。
ViewModelの生成については
https://github.com/android/architecture-components-samples

https://qiita.com/satorufujiwara/items/f42b176404287690f1d0
を参考にするのが良いでしょう。

ViewModelを使ったことならわかると思いますが、
「コンストラクタによる値渡しをするには独自ViewModelProvider.Factoryを継承したクラスが必要」
です。
各ViewModelのコンストラクタの値をDIするためには、ViewModelProvider.Factoryを継承したクラスをDIする必要が出てくるわけです。

そこで、以下のようなViewModelFactoryクラスを用意します。

MyViewModelFactory.kt
class MyViewModelFactory
@Inject constructor(
    private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = creators[modelClass] ?: creators.entries.firstOrNull {
            modelClass.isAssignableFrom(it.key)
        }?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

コンストラクタで渡されているcreators: Map<Class<out ViewModel>は、
先程のHomeModuleのViewModelのアノテーション@IntoMapをつけたことでDIされているViewModelが入っています。

    @Binds
    @IntoMap ←これ
    @ViewModelKey(HomeViewModel::class)
    abstract fun bindHomeViewModel(homeViewModel: HomeViewModel): HomeViewModel

そして、@IntoMapで入ったHomeViewModelのキーとして@ViewModelKeyでClassを渡しています。
ちなみにこのViewModelKeyは先程の
https://github.com/android/architecture-components-samples
にもありますが、独自アノテーションクラスです。

ViewModelKey.kt
@MustBeDocumented
@Target (
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

これは僕は完全にコピペしました。
ようするに、ViewModelをDIするためにはViewModelFactory継承クラスが必要で、上述したようなViewModelFactoryクラスを作ってDIさせてあげる必要があります。

なので、AppModuleなどでBindsもしくはProvidesしてあげましょう。

AppModule.kt
@Module
abstract class AppModule {
    @Binds
    abstract fun bindViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}

利用する場合は以下のように利用します。

HomeFragment.kt
class HomeFragment : DaggerFragment() {
    @Inject
    private lateinit var homeViewModelFactory: MyViewModelFactory

    private val homeViewModel: HomeViewModel by viewModels {
        homeViewModelFactory
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.fragment_home, container, false)
        val textView: TextView = root.findViewById(R.id.text_home)
        homeViewModel.text.observe(this, Observer {
            textView.text = it
        })
        return root
    }
}

ここではViewModelをbyで呼び出す新しい方法を使ってます。
今まではどうしてもlateinit varで定義して、onCreateで
ViewModelProviders.of(this).get(XXX::class.java)
のように書かないといけなかったので、非常に書きやすく便利になりました。

ということで、これでActivity→Fragment→ViewModelまでDIができるようになったはずです。

ViewModel以降のDIについては、各アーキテクチャに従うことになると思います。
僕のなんちゃってアジャイルでやる場合は、ViewModelさんは必要なドメインとなるApplicationServiceクラスをコンストラクタに保持することになるので、ViewModelのconstructorをinjectすることになります。

引数付きconstructorのDIの仕方を書きたいですが、この記事は少し長くなってしまったので、別途書きたいと思います。

Dagger2を使ってみた感想

正直、DDDを使ってレイヤー化させていた僕としては、DIができるのは非常にやりやすかったです。魔法なのかと。
ただ、お作法があるので、それを守っていないとコンパイルエラーになるので、結構ハマることが多かったです。

最後に

みなさんも快適なDIライフをお送りください。
ここまで読んでくださった方ありがとうございます。ぜひ間違ってる部分などあると思うので、フィードバックいただけると幸いです。

11
10
1

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
11
10