Daggerに入門した
GDG DevFest Okayama 2019に参加した際、STAR_ZEROさんに色々Daggerに入門するときのリソースを教えてもらったので、Android Daggerに入門してみました。(やっと個人的な宿題を一段落させた気持ち…)
具体的にはCodeLabsのUsing Dagger in your Android appを教えてもらいました。yanzmさんのMaster of Daggerも教えてもらったんですけど、英語を勉強中なのと、公式のリソースだけで進めたい気持ちもあり、どれを読むかを悩んだ結果CodeLabsを選びました。 今回はそのときに学んだ内容のメモを書きます。ほぼ超訳です。主にDaggerで使用するAnnotations周りのメモとなります。
僕がDaggerを勉強したいなと思ったのは、ユニットテストをやりたかったからです。 僕は今DIを使っていないプロジェクトを触っていて「ユニットテストを書きたい」気持ちがわりとあります。しかし、どうしてもユニットテストをするために書かなければならないコードが多い、かつmockにしないといけないオブジェクトが多いために、テストが書き始めれない…となっているところです。DIを使うと、クラスをインスタンス化する時に渡す必要のあるインスタンスを必要なものだけにできるので、モックにするものを限定的にすることが可能です。そこに魅力を感じています。
Daggerについて
DaggerはDependency Injection(DI)のためのライブラリです。Androidだけでなく、Javaプロジェクトでも利用できます。 「Daggerを使用することで、コードの再利用性を高め、リファクタリングやテストを容易にします」とのこと。 「DIって何?」という箇所に関しては、Dependency injection in Androidやその他リソースを読むのがよさそうです。
基本的なAnnotations
@Inject
二種類あります。
- コンストラクタにつける
@Inject
class RegistrationViewModel @Inject constructor(val userManager: UserManager) { ... }
- field injectionでの
@Inject
class LoginActivity: Activity() { @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // ① #inject(LoginActivity) で、LoginActivity内の@Injectのついたフィールド変数にDaggerによってインスタンス化された値が注入される (applicationContext as MyApplication).appComponent.inject(this) // ②この時点で、 this.loginViewModel にはインスタンスが注入されている super.onCreate(savedInstanceState) } }
@Component
@Component interface AppComponent { ... fun inject(activity: LoginActivity) }
- 上記の例であれば、依存を注入して欲しい
- インターフェースメソッドのパラメータは、Daggerに何を注入して欲しいか伝えるためのもの
- LoginActivityには、
var loginViewModel: LoginViewModel
の宣言に、@Inject
をつけた。この場合、Daggerは、LoginActivityをDaggerコンポーネントであるAppComponent
の#inject()
メソッドにLoginActivityを渡すことで、DaggerにLoginViewModelを注入して欲しい事がわかる
- LoginActivityには、
AppComponent#inject(this)
をActivityで呼び出す場合、super.onCreate
よりも前に書く必要がある。Fragmentの再生成時の問題を回避するため。- ただし、Fragmentに書く場合は、onAttach()メソッド内の
super.onAttach
よりも後であること。
- ただし、Fragmentに書く場合は、onAttach()メソッド内の
@Module
- classに対して@ModuleをつけたものをDaggerモジュールと呼ぶ
- Dagger Moduleは、Daggerがどうやってインスタンスを用意するかを、Daggerに教えるもの
@Provides
,@Binds
,@BindsInstance
を使ってインスタンスを提供する方法を定義できる
- Dagger ModuleはDaggerがアプリケーショングラフを構成するのために、@Componentを付与しているインターフェースに対して
@Component(modules = [StorageModule::class])
のようにDaggerに教える必要がある - モジュールは、オブジェクトを提供する方法を隠す手段である
@Binds
- インターフェースを用意する手段をDaggerに教えるためのもの
- もっと複雑な教え方をすることも可能(CodeLabsでは触れていない)
- 必ずabstractメソッドにする必要がある
- 返却の型を、用意したい型とすること
@Module abstract class StorageModule { // Storageクラスを要求した場合、DaggerはSharedPreferencesStorageを用意させる @Binds abstract fun provideStorage(storage: SharedPreferencesStorage): Storage }
@Provides
@Provides
はGitHubBrowserSampleのAppModule.ktを見るのがわかりやすい
@BindsInstance
Context
のように、Daggerによって生成できないインスタンスもある。このアノテーションを使って、例えばContext
をDaggerにインスタンスを渡しておくと、Daggerが何らかのインスタンスを生成する際、インスタンスがContext
を要求した場合、渡しておいたContext
インスタンスを使ってインスタンスを生成する
スコープ
- Daggerでは、@Injectの対象のインスタンスは、毎回Daggerによって生成される。これがデフォルトの動き。
- しかし、毎回newするのではなく、同じインスタンスを複数の画面で使いまわしたい場合がある。この場合に使うのがスコープ。
@Singleton
- これをクラスに着けると、必ずアプリケーショングラフにつき一つのインスタンスとなる
@Singleton class UserManager @Inject constructor(private val storage: Storage) { ... }
@Subcomponent
- Activity単位でインスタンスを持たせたい、といった場合に使用するのがDaggerサブコンポーネント
- サブコンポーネントは親コンポーネント(今回でいうと
AppComponent
)を継承している。つまり、親コンポーネントで用意されたオブジェクトは、子コンポーネントでも用意されている状態である。 - サブコンポーネントの定義は少しややこしいのでSubcomponentsを参考
@Scope
- スコープであることを示す
- これをつけた独自のアノテーションを作ることで、自分でスコープを作ることが可能
所感
DaggerのDocumentationを読んでて思ったのは、「なんか便利そうだから入れてみる」みたいなノリで入れると、「DI使うのかい?使わないのかい?どっちなんだい?」って感じになって地獄を見るかもしれないので、基礎を勉強した上で(チームでの開発をしている場合はチームを説得するなりして)入れるのがよいかなって思っています。 あと、もし既存プロジェクトにDaggerを導入する場合は、まずはリファクタリングから始めなければならないかも知れません。どのようにリファクタリングするかは、Daggerの基礎を読み、ゴールとなるapplication graph(アプリで使用しているクラスの依存関係)を考え確立したうえで、導入した方がよさそうです。
Daggerは情報量が多く、ドキュメントが色んな所に散らばっている上に、古い情報も残っていたりして、かなりいろいろなところを読まないといけない状態になっています。これは学習コストをさらに引き上げている一つの要因のような気がします…。
おまけ:dagger-androidについて
DaggerをAndroidプロジェクトに導入するには、下記をbuild.gradleに入力すればOKです。
dependencies { ... implementation "com.google.dagger:dagger:2.24" kapt "com.google.dagger:dagger-compiler:2.24" }
これはCodeLabsのapp/build.gradleを見ると分かります。
ただ、僕は終わってから気づいたんですが、CodeLabsの内容は、Daggerの基本のみです…。このままでもプロジェクトへの導入は可能ですが、もっと効率的にAndroidのプロジェクトでDaggerを使うには、dagger.dev/androidにある、dagger-androidやdagger-android-supportといったライブラリの使い方も覚えたほうが良いです。Android Architecture Component - GitHubBrowserSampleには、実際にこれらのライブラリを使ったサンプルがあります(基礎がわかってないとソースみてもちんぷんかんぷんです)。
dagger-androidを含めたDaggerライブラリ全体を入れるには、下記のコードをbuild.gradleに入れる必要があります。
dependencies { ... implementation "com.google.dagger:dagger:2.24" implementation "com.google.dagger:dagger-android:2.24" implementation "com.google.dagger:dagger-android-support:2.24" kapt "com.google.dagger:dagger-compiler:2.24" kapt "com.google.dagger:dagger-android-processor:2.24" }
↑のようなdaggerの基本ライブラリからdagger-android-supportまでを含めたすべてのdependenciesの導入方法は、GitHubBrowserSampleの中身を見て調べました。公式に書いてそうなんですが、僕自身がその導入方法を書いてるところを見つけれていません。。 (CodeLabsは基本だけなのでdagger-android-supportに関する記述がないし、Dagger公式のHow do I get it?セクションは古いし…。)