自分の担当するプロジェクトで全然Daggerに触れる機会がないので、いい機会なのでDagger2を今更導入してみました。
きっかけ
自分が今取り組んでいるDDDにおいて、各レイヤーごとに処理を分ける必要がありますが、プレゼンテーション層から下のレイヤーのインスタンスを作るのが非常に手間だったからです。
DIを利用して依存性を注入してあげれば、ActivityやFragmentから
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にライブラリを指定します。
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するという方法もあるかもしれません。(多分)
では、実装例です。
実装例
@Singleton
@Component(modules = [
AndroidSupportInjectionModule::class,
MainModule::class
])
interface AppComponent: AndroidInjector<App> {
@Component.Factory
abstract class Factory : AndroidInjector.Factory<App>
}
説明
インタフェースの作成
AppComponent
はInterface
で定義します。
定義したら、@Component
のアノテーションを付与します。
@Component
内にmodule = []
を記述し、関連するModuleを指定してあげます。
ようするに、ここは各Moduleで設定しているインスタンスを作る窓口的な役割です。
ここでは
AppModule
、AndroidSupportInjectionModule::class
の2つを@Component
のmodules
に指定します。
AppModule
については後述します。
継承元について
今回、AndroidInjector<T>
を継承しています。
T
にDIさせる場所を指定することでDIが出来ます。
今回はApp
としていて、独自アプリケーションクラスを指定していますが、MainActivity
など、
Activityを指定することも可能です。
その代わり、指定したクラスでのインジェクトが必要になるので、DIする場所に気をつけてください。
抽象クラスFactoryの定義
インタフェース内に、AndroidInjector.Factory
を継承したクラスを定義し、
@Component.Factory
アノテーションを使用しています。
他の記事にもありますが、@Component
にはFactory
やBuilder
があり、
そのクラスを定義してあげることで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
を作りましょう。
Moduleを作る
AppComponentができたので、次はModuleを作りましょう。
まずサンプルとしてAppModule.ktというもの作ります。ここでは仮のRepositoryクラスをDIさせます。
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()
}
}
@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です。
@Singleton
@Component(modules = [
AndroidSupportInjectionModule::class,
MainModule::class,
AppModule::class // 追加する
])
interface AppComponent: AndroidInjector<App> {
@Component.Factory
abstract class Factory : AndroidInjector.Factory<App>
}
ここまでやったら、あとはAppModuleでインスタンスを作ったものをインジェクトするだけです!
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してみたいと思います。
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を作ります。
@Suppress("unused")
@Module
abstract class HomeActivityModule {
@ContributesAndroidInjector
abstract fun contributeHomeActivity(): HomeActivity
}
これをAppComponentに指定します。
@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を仕込んだのでログを確認してみます。
ログなし。
よくわからんので調べてみました。
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です。
@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クラスを用意します。
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
にもありますが、独自アノテーションクラスです。
@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してあげましょう。
@Module
abstract class AppModule {
@Binds
abstract fun bindViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
利用する場合は以下のように利用します。
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ライフをお送りください。
ここまで読んでくださった方ありがとうございます。ぜひ間違ってる部分などあると思うので、フィードバックいただけると幸いです。