LoginSignup
48
22

More than 3 years have passed since last update.

Dagger Hilt: Deep Diveのメモ

Last updated at Posted at 2020-09-23

近々Dagger Hiltについて話す機会があるので、少し調べています。
また、最近のGoogleの動画からいろいろインプットしていっています。
Deep Diveですが初心者にも分かりやすいのではと思いました。1時間半ある動画でしたが、かなり気になるところを色々話してくれています。個人的にDaggerの説明でよく使われるCoffeeShopの例よりも100倍分かりやすい例えで説明してくれているので、ぜひ見てみてください!
https://www.youtube.com/watch?v=4di2TTqeCrE

なぜDependency Injection?

データーベースなどがハードコードされている例

class MusicPlayer {
  private val db = SQLiteDatabase()
  private val codecs = listOf(CodecH264(), CodecFLAC())
  fun play(id: String) { ... }
}
fun main() {
  val player = MusicPlayer()
  player.play("...")
}

コンストラクタを呼び出して、play()を呼ぶだけ。シンプル。依存関係もハードコードされているので何もしなくて良い。

ただ、たくさんの問題が起こる。
MusicPlayerを再度使いたい場合、データベースがハードコードされているので別のMusicPlayerでは使えなかったり、違うコーデックが使えなかったりする。
またそれらを実行時や他のなにかでも交換できない。
テストも問題になる。データーベースがハードコードされているので、例えばメモリ内のDB(In memory database)を使いたい場合は不可能となる。
またこれはさまざまな依存関係やクラスの動作を一目で確認できないため、リファクタリングを難しくする。

Construction InjectionによるDependency Injectionの例

コンストラクタで依存を受け取るためConstruction Injectionと呼ばれる。setterで渡すsetter injectionもある。

class MusicPlayer (
  private val db: Database,
  private val codecs: List<Codec>
)
  fun play(id: String) { ... }
}

上記の場合、初期化時に明示的に依存関係が書かれているので、以下のように先に他のオブジェクトを作る必要がある。
このようになにかオブジェクトを作るときにはその依存関係を作る必要がある。

fun main() {
  private val db = SQLiteDatabase()
  private val codecs = listOf(CodecH264(), CodecFLAC())
  val player = MusicPlayer(db, codecs)
  player.play("...")
}

ただ、このようなMusicPlayerをなんども手動で依存関係も作る場合はボイラープレートとなりえる。

コンストラクションロジックとは = ただ、他の型やクラスを作るためのロジック
ビジネスロジックとは = アプリケーションの価値を作るもの

  • ビジネスロジックからコンストラクションロジックを分離するべき。
    コードを追ったり、読んだりするのを難しくする。またそれはクラスを読む人にとってあまり意味のないものである。
  • Dependency Injection(DI)では、DIコンテナがこの役割。
    DIではコンストラクションロジックはビジネスロジックから分離される。
    separation of concerns(関心事の分離)やsingle responsibility principle(単一責任の原則)があるが、この責任のあるクラスがDIコンテナとなる。

DIについてまとめ

  • 良い利益があるので、GoogleはDependency Injectionを推奨
  • ボイラープレートを避けるためにDIライブラリが推奨される。

なぜAndroidではDIが難しいのか?

FrarmeworkのクラスがAndroidのSDKの中で作られてしまうため。
API 28ではFactoryが追加されているが、現実的ではない。これがDaggerやHiltをおすすめする理由。

Daggerについて

去年はDaggerをおすすめしたが、今はHiltがおすすめ。
Daggerはおすすめしたが、もっといい解決策を探した。Daggerは設定が難しい。またDaggerでは同じことをする方法が複数存在する。
49%の開発者が、より良いDIの方法を探していた。

どのような解決策を望んでいたか?

  • Opinionated(意見を持った。)
    決めるのを楽にして、使うのを楽にする (後述)
  • セットアップが簡単(DaggerはAndroidではなく、Java用だったので、Daggerは設定が難しかった)
  • 重要な部分にフォーカスできる

これがメインのHiltが生まれた理由。

Dagger Hilt

Dagger HiltはDaggerの上に構築されたライブラリ。
Daggerの良いところを使うことができる。またAndroidXチームとDaggerチームで共同で作られている。

  • AndroidにおけるDIの標準化
    どのようにAndroidでDIするのかを標準化(standardize)する。
  • Daggerの上に構築されている。
  • アノテーションベース
    Hiltに対して、アノテーションで何をしたいのかを伝える。
  • ツールサポート
    Android Studio 4.1以降では左側にガターアイコンが表示される。例えば依存関係がどこから来たのかがわかる。
  • AndroidX Extension
    ViewModelとWorkManagerで使える。他のライブラリも追加予定。

Setup

どうやってMusicPlayerを組み立て方をHiltに伝えるのか?
コンストラクタを変更できるのであれば、以下のように @Injectをコンストラクタにつける。これが基本的なHiltへこのオブジェクトの作り方の教え方となる。このクラスの依存関係は、以下のコードでは現状はなにもない。何も依存がないため、Hiltはこのクラスを明らかに作ることができる。

class MusicPlayer @Inject constructor() {
  fun play(id: String) { ... }
}

そして、別のアノテーション @HiltAndroidAppはApplicationクラスにつける。これはHiltとAnnotion Processorに"このアプリはHiltを使うぞ"と教える。そして、これはdependency injection container(DIコンテナ)をアプリケーションクラスに追加する。DIコンテナはどのようにオブジェクトを作るのかのロジックとインスタンスを持っている。

@HiltAndroidApp
class MusicApp : Application()

もしHiltをActivityで使いたい場合。
Activityではコンストラクタにアクセスできないということを言ったが、そのため@AndroidEntryPointをActivityにつける。
これによって、3つHiltに教える。

  • "このActivityはInjectionを行う。このActivityはDependencyInjectionを使う"
  • "別のdependency contaienrをこのActivityに追加。"
@AndroidEntryPoint
class PlayActivity : AppCompatActiivty() {
  • HiltからDependencyを持ってくる
    変数に@Injectアノテーションを付けている。これはHiltからInjectされることを意味する。
    "Actiivtyが作られたときに、MusicPlayerをInjectして" と言っていることになる。
    そして、onCreateメソッドはMusicPlayerを呼ぶなど好きなことができる。
@AndroidEntryPoint
class PlayActivity : AppCompatActiivty() {
  @Inject lateinit var player: MusicPlayer

  override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    player.play("...")
  }

もう少し複雑な例

MusicPlayerがMusicDatabaseに依存しているとする。
Hiltが、MusicPlayerを作るためには、どのようにMusicDatabaseを作ることができるのかを知っている必要がある。
なぜならMusicDatabaseはMusicPlayerの依存となるため。

class MusicPlayer @Inject constructor(
  private val db: MusicDatabase
) {
  fun play(id: String) { ... }
}
  • どのようにHiltにMusicDatabaseを作るかを教えるか?

→ もしMusicDatabaseのコンストラクタを変えられるのであれば、MusicPlayerと同じように コンストラクタで@Injectを使うことで解決できる。

しかしこのDatabaseがJetpackライブラリのRoomから作られたりなど別のところから来る場合はどうだろうか?
Roomの場合はコンストラクタにアクセスすることができず、@Injectアノテーションを付けることができない。

→ この場合はモジュールを使う必要がある。モジュールはただのクラスで、@Moduleアノテーションを付ける。Installingと呼ばれるアノテーションを付ける。
モジュールにはメソッドを追加する。
モジュールのメソッドはレシピだと考えれば分かりやすい、この型をビルドするレシピはこれ!みたいな。基本的にこれはこのモジュールでレシピをHiltに教えている。
そして、この情報をApplicationComponentに置く(= Application LevelのDependency Containerに置く)

@Module
@InstallIn(ApplicationComponent::class)
object DataModule {
...
}

@Providesがついたメソッドが書いてある。ここではHiltにどのようにMusicDatabaseを作るのかを教えている。
これによってHiltがDatabaseの作り方を知る必要があるときに、レシピの情報がここにあるので、ここに来て、このコードを実行して、依存関係を提供する。

@Module
@InstallIn(ApplicationComponent::class)
object DataModule {
  @Provides
  fun provideMusicDB(@ApplicationContext context: Context) =
    Room.databaseBuilder(context, MusicDatabase::class.java, "music.db")
      .build()
}

Component

ApplicationComponentなどが登場しましたが、これについてもう少し見てみよう。
Application ComponentなどはDIコンテナとして考えられる。
説明したようにこのコンテナはどのようにオブジェクトをビルドするのかのロジックをハンドルする。
例えばMusicPlayerにはMusicDatabaseが必要になるなど、Componentはインスタンスの生成の順序などのロジックを持っている。

これがどのように内部での仕組みになっているのか?
たくさんのファクトリーパターンに従ったファクトリーがある。そのファクトリーはどのようにインスタンスを作るかを知っている。
例えば、MusicDatabaseのファクトリーを持っていて、他にMusicPlayerのファクトリーもある。そして、それをつなげて一緒に動くようにする。
このファクトリーはHiltによって明らかに小さくなった。そしてファクトリーが必要になるまでは使われない最適化もされる。また、スコープなどによってインスタンスを使い回すロジックもある。

HiltはなぜOpinionedなのか?

Dagger HiltにはComponent、DIコンテナが付随するため。
ApplicationComponentやConfigrationChangeでも生き残るActivityRetainedComponent、ActivityComponent、そしてFragmentComponentが付随する。
ApplicationやActivityはフレームワークのコンポーネントなので、ライフサイクルがある。そしてApplicationComponentはApplicationクラスのライフサイクルに従う。AcitivtyComponentはActivityのライフサイクルに従う。
そしてサービスやViewといった特定の部分のComponentもある。

スコープ

(端折り気味)
現状、MusicDatabaseのインスタンスがInjectされるたびに、ModuleのprovideMusicDBによって配布されてしまうため、2つActivityがあって両方でMusicDBがInjectされていた場合、合別々のインスタンスが保持されてしまう。しかしConnectionを共有したかったり、同期だったりで一つのインスタンスを共有したかったりする。

@Module
@InstallIn(ApplicationComponent::class)
object DataModule {
  @Provides
  fun provideMusicDB(@ApplicationContext context: Context) =
    Room.databaseBuilder(context, MusicDatabase::class.java, "music.db")
      .build()
}

これをどのようにHiltに伝えればよいのか?
@Singletonアノテーションを付ける必要がある。クラスか、コンストラクターか、モジュールの関数で使う。これがスコーピングアノテーションとなる。
これを使うことでインスタンスが共有される。このようなスコープのアノテーションは@Singletonの他に@ActivityRetained@ActivityScopedなどそれぞれのコンポーネントが違ったComponentを持っている。

@Module
@InstallIn(ApplicationComponent::class)
object DataModule {

  @Provides
  @Singleton // **←**
  fun provideMusicDB(@ApplicationContext context: Context) =
    Room.databaseBuilder(context, MusicDatabase::class.java, "music.db")
      .build()
}

Entry Point

Hiltによって作られた依存関係とHiltの依存関係以外の部分から取得するのは直接Hiltではサポートされていない。
そういうところにEntry Pointが使われる。
基本的に依存関係のグラフやDIコンテナににアクセスすることに使える。
例えば他のアプリとのコンテンツの共有に使われるContentProviderはApplicationのonCreateより先に呼ばれてしまったり、少しライフサイクルが特殊だったりするので、Hiltでサポートされていない。しかしContentProviderでMusicDatabaseを使いたい場合は存在する。MusicDatabaseはApplicationComponentから取得できることがわかっている。このような場合にEntryPointが使われる。
EntryPointはただのinterfaceでEntryPointのアノテーションがついており、どこから情報を取得するかをInstallInで指定する。
基本的にHiltはfun getMusicDatabase(): MusicDabaseに対してコードを生成してくれる。

class PlaylistContentProvider: ContentProvider() {
  @EntryPoint
  @InstallIn(ApplicationComponent::class)
  interface PlaylistCPEntryPoint {
    fun getMusicDatabase(): MusicDabase
  }
}

そして実行時は、EntryPointAccessorsを使って、このサンプルではApplicationコンポーネントを使って、Interfaceのクラスを渡して、Hiltが生成したinterfaceの実装を取得できる。そしてgetMusicDatabaseを使ってHiltの中にある依存関係を取得できる。

class PlaylistContentProvider: ContentProvider() {
  @EntryPoint
  @InstallIn(ApplicationComponent::class)
  interface PlaylistCPEntryPoint {
    fun getMusicDatabase(): MusicDabase
  }

  override fun query(...) {
    val entryPoint = EntryPointAccessors.fromApplication(
      context,
      PlaylistCPEntryPoint::class.java
    )
    val database = entryPoint.getMusicDatabase()

    // ...
  }
}

質問コーナー

内部的にSubComponentかdependenciesかどちらが使われるか?

(DaggerはComponentの依存関係を作る方法が2パターンある)
SubComponentのほうが技術的に楽なので、SubComponentが使われている。
Dynamic Feature Moduleでは依存関係が逆になるこれまでDaggerではGoogleはdependant moduleを推奨していた。
Hiltを使っている場合も、そのDFMの部分はDaggerで書く必要がある。
https://developer.android.com/training/dependency-injection/hilt-multi-module?hl=ja

上記の問題を解決する予定ある?

Dynamic Feature Moduleとアノテーションプロセッサーが相性が良くない。他のライブラリでも同じ問題が起こっている。例えばSafeArgsやNavigation。メタデータをexportするなどを考えているが現状はできない。

1つのモジュールではなくモジュールを分ける必要ある?

スケールしたり、他のチームと一緒に仕事したり、関心事の分離など。

AndroidX Extension

Dagger HiltはActivityやFragmentと一緒に動くが、他のComponentとも動くようになっている。例えばViewModelやWorkManager。ViewModelを例に見ていく。
ViewModelではコンストラクタにアクセスできるが、ViewModelでは直接インスタンスを作らず、Provider、Factoryなどを通じてインスタンスを作成する。なぜならConfiguration changeを通じて生き残るためである。
HiltはこのためのProviderなどのコードを自動生成する。そのため、ただ一つの@ViewModelInjectアノテーションが必要となる。プロセスが死んだり、Activityの再生成でsaved state handleを使いたいとき、通常フレームワークが作るので持っていない。しかしHiltでは @Assistedアノテーションを付けることで何もする必要はなく必要なときに利用できる。

class PlayViewModel @ViewModelInject contructor(
  @Assisted val savedStateHandle: SavedStateHandle
  val db: MusicDatabase
) : ViewModel() {
...
}

ViewModelを使うときには @AndroidEntryPointがついたAndroidのコンポーネントでktx extensionを使っているとby viewModels()を使うことができ、それを自由に使うことができる。AndroidX extensionと簡単に連携ができる。

@AndroidEntryPoint
class PlayActivity: AppCompatActivity() {
  val viewModel: PlayViewModel by viewModels()
}

Testing

Hiltがどのようにテストをシンプルにするのか?
テストをするためにはPlayerを作る必要がある。そのためその依存関係のDatabaseを作る必要があり、そのときにはメモリに保持するDBがほしい。基本的にはそのインスタンスをただ作って、MusicPlayerに渡せばテストができる。なぜならこれでMusicPlayerのテストするためのインスタンスができるため。

@RunWith(AndroidJUnit4::class)
class MusicPlayerTest {
  @Test
  fun testPlay() {
    val db = Room.inMemoryDatabaseBuilder(...)
    val player = MusicPlayer(db)
    player.play("...")
    assertTrue(player.isPlaying())
  }

依存関係が1つしかないため、今はこれで大丈夫だが、依存関係が20個ある場合はどうだろうか?そしてそれぞれの依存関係が推移的に依存を持っている場合(依存が他のものに依存している)、それをすべて作る必要がある。
それは問題となりえ、依存関係を切るためにmockやfakeを作り始める。しかしそれには良し悪しがある。

Hiltは基本的にすべてのコンストラクションロジックを持っていて、テストでも持っているため、基本的に行うことができる。
HiltAndroidRuleを使って、コンポーネントの作成とそれらのコンポーネントの管理を行う。そしてActivityや他の場所と同じようにinjectを使うことができる。MusicPlayerをInjectできる。

@HiltAndroidTest
@RunWith(AndroidJunit4::class)
class MusicPlayerTest {

  @get:Rule
  val rule = HiltAndroidRule(this)

  @Inject
  lateinit var player: MusicPlayer

  @Before
  fun setup() {
    rule.inject(this)
  }

  @Test
  fun testPlay() {
    player.play("...")
    assertTrue(player.isPlaying())
  }
}

しかし、このままではInMemoryDatabaseに切り替えることができない。
そのため、HiltにDatabaseを作るレシピを忘れさせ、メモリのDBを使うようにする。
@UninstallModuleによってDataModuleの内容を忘れさせて、Databaseの作り方を忘れさせる。

@HiltAndroidTest
@UninstallModule(DataModule::class)
@RunWith(AndroidJunit4::class)
class MusicPlayerTest {

そしてここでMusicDatabaseの情報を教える必要があるので、テストの中でModuleを作る。このテストのための依存関係が追加できる。もしこのモジュールを外に持っていくとこれは、すべてのテストに適応される。

@HiltAndroidTest
@UninstallModule(DataModule::class)
@RunWith(AndroidJunit4::class)
class MusicPlayerTest {
  @Module
  @InstallIn(ApplicationComponent::class)
  object TestMoudle {
    @Provices
    fun provideDB(app: Application) =
      Room.inMemoryDatabaseBuilder(...).build()
  }

テストランナーなどの設定が必要なので、ドキュメントを確認すること。

マイグレーション

Dagger HiltはDaggerやDagger Androidとともに動く。
HiltはマイグレーションAPIが存在する。
マイグレーションガイドもある
https://dagger.dev/hilt/migration-guide

将来の変更

  • Android Gradle module以外でも動くように。
  • Assisted Inject
    view moduleにidを渡したいときなど
  • 他のAndroidXのコンポーネントとの連携 navigationなど

質問

stableになるには何が足りないのか?

いくつかがある。
Android Gradle Plugin外で動くようにしたり、Assited Injectionなどをstableにする前に入れたい。
プロダクションで準備はできている。なぜなら実際にGoogleのアプリでは使われているため。そしてオープンソースのアプリをhiltにマイグレーションしている。例えばsun flower、google io、tv from Chris Banesで使われている。

パフォーマンス、kaptを使わないといけないのでスピードが出ないのでは?

Hiltによる追加の時間は多くない。Hiltは基本的にレイヤーが薄く、Dagger上にあるため。
HiltはDaggerのためのコードを生成するが、生成するコードの量は多くはない。
kaptが遅いのは分かっていて、Daggerチームはannotation processorの代わりにksp(Kotlin Simbol Processing)を使う方法を探っている。しかし、それがいつできるかはわからない。
現在コードの生成にJavaPoetやそれに似たものを使っていて、同じ文法でkspとjavaコード両方を書くことができるようにしたいと考えている。ただJavaとKotlinを両方同時にサポートするため。これがHiltでkspを使う前にしたいことになる。

なぜHiltにGradle Pluginが必要なのか

Gradle Pluginは任意で、技術的ドキュメントを確認すればGradle Pluginなしで適応する方法が書いてある。@AndroidEntryPointアノテーションのためにある。
実際HiltはMusicPlayerActivityがあればHiltMusicPlayerActivityをバイトコードトランスフォーメーションで生成し、それをMusicPlayerActivityで継承するようにする。もし、Gradle Pluginを使わない場合はそのクラスを作る必要がある。

それってコンパイル時間かかる?
この処理ははやい

インクリメンタルビルドについては?
すでにincremental buildと一緒に動作する。

KoinやKodeinからマイグレートするべき?

KoinやKodeinは解決策で、Googleは何かを推薦するが、あなたが使いたいものを使うことができる。しかし、もし、KoinやKodeinでうまく言っていたときに他の人がプロジェクトに入ってきたときに、Hiltが標準の解決策なので、もしHiltであればプロジェクトを移動したときに同じ解決策でできるので新しい技術を学ぶ必要がない。しかしマイグレーションはあなたやチームによるので、これは必須ではない。

DaggerにHiltのための機能追加とかした?

していない。

なぜFragmentRetainedComponentがActivityRetainedComponentと同じようにないのか?

これは非常に複雑な質問。ダイヤモンド問題が起こる。
ActivityRetained <- Activity <- Fragment
のようになっていて、Fragment retained componentを追加したいが、Fragment retained componentはActivity retained componentやActivity comopnentを継承したいが、実際にはそこに循環が発生し、避けられない。
Fragment factoryを作るためにFragmentRetainedComponentを考えた。ダイヤモンドを作成し、リンクを切るというもの、しかし、現状ActivityComponentとActivityRetainedComponentなど複数の親を持てないのが問題になる。

square/anvilについて(同じようなことができるライブラリ)

Dagger Hiltは2つの部分からなる

  1. @InstallInアノテーションによるモジュールのAuto discoverabillity
  2. @AndroidEntryPoint コンポーネントを自分で作る必要がなく、自動的にInjectされる

amvilでは1つ目のみを行う。
チームもこの2つのアーティファクトに分離することは考えている。

Hiltは大きなアプリでも使えるか?

実際Googleのアプリは大きいがHiltを使っている。DaggerとHiltはスケールする。Dagger Hiltで対応するコンポーネントがあるため対応もかんたん。

@ViewModelInjectみたいにViewModelのInjectはできるが、FragmentはFactory作れて、Construction Injectionできるけど、やらないの?

まだ実装されていない。ダイヤモンド問題がある。また他にも問題があり、Fragmentを作るときにFragment Factoryが必要になるが、これはonCreate前には使われない。そしてFragmentを作るときにFragmentのインスタンスを使うことができなくなる。
またActivityComponentにFragment Factoryを実装すると、Fragment FactoryはFragmentの情報を使えないが、Fragmentでは使えることになるので、混乱を生む。
Fragment Factoryとの連携のエッジケースが起こらない堅牢な方法を探している。

lateinitや@InjectアノテーションだとNullPointerException起こる可能性あるけど消すプランはある?

Kotlinでlateinitをとても使っている。たぶん今そのようなFeature requestがないので、それをfileし、property delegateなどの方法を考えることはできる。

HiltでFileにアノテーションを付けてtop level functionで依存関係を配布とかできたりする?

いいアイデアだと思う。Feature reqeustにfileしたほうが良さそう。Daggerは長い間Javaのライブラリとして存在しており昔のメンテナは他のプロジェクトに移っており新しい人々が貢献している。Kotlinと共によく動くようにしていこうとしている。

Android以外でどのぐらいDaggerって使われているの?
むずかしい。わからない。

Dagger 3.0みたいなの考えている?

この年動き始めるかも。個人的にHiltかDIをComposeに入れるために。
Jetpack Composeに今使えるAmbientはとてもサービスロケーターっぽい。そのため、どのように適切にDIを行うのか。Hiltをその中で使いたいと考えている。

ViewModelにidとか渡せる?

今はサードパーティのAssitedInjectで行う。sunflowerでもAssisted Injectを使っている。
今後なおしたいと考えている。

(省略)

Multibindingを楽にできない?

難しいけど、エスケープハッチのように拡張性を作ってくれる。

48
22
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
48
22