Spica*

プログラミングの話。

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
    • Daggerが、どうやってインスタンスを作るかを知るために必要なアノテーション
    • これをクラスのコンストラクタにつけることで、@Injectをつけたコンストラクタの引数は、そのクラスの依存であることをDaggerが知ることができる
    • これはDaggerが生成できるインスタンスに限ってつけることが可能。例えばAndroidではActivity/Fragmentなどはシステムが生成する。Dagger自身が生成することはできない。Activity/Fragmentなどは、代わりにfield injectionを使用する。
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}
  • field injectionでの@Inject
    • Androidフレームワークは、システムによってActivityやFragmentが生成される。そのため、Daggerによってインスタンスを生成できない。そのため、Field Injectionを使う。
    • 注入したいフィールド変数の宣言に@Injectをつける
      • @Inject lateinit var registrationViewModel: RegistrationViewModel のような感じ
      • この時、宣言にprivate修飾子をつけないこと
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を注入して欲しい事がわかる
  • AppComponent#inject(this)をActivityで呼び出す場合、super.onCreateよりも前に書く必要がある。Fragmentの再生成時の問題を回避するため。

@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

@BindsInstance

スコープ

  • Daggerでは、@Injectの対象のインスタンスは、毎回Daggerによって生成される。これがデフォルトの動き。
  • しかし、毎回newするのではなく、同じインスタンスを複数の画面で使いまわしたい場合がある。この場合に使うのがスコープ。

    @Singleton

  • これをクラスに着けると、必ずアプリケーショングラフにつき一つのインスタンスとなる
@Singleton
class UserManager @Inject constructor(private val storage: Storage) {
    ...
}

@Subcomponent

所感

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?セクションは古いし…。)