LoginSignup
21
16

Hiltを自習したときのメモ

Last updated at Posted at 2021-07-25

プロジェクトでの Hiltの採用にあたり、 Daggerで全く理解できてなかった DIについて自習したときのメモ。

間違いがあったら指摘してください。

Dependency Injectionを使う動機について

「Carは Engineを使用する」 という仕様について

class Car {
  val engine = Engine()
}

みたいに書くと、 正常系はいざしらず、Engineの挙動に色々障害を起こさせてテストするのにいちいちコードを修正しなきゃいけなくって、それを書き戻すことを忘れたら悲惨なことになるよね、という

class Car {
  // val engine = Engine()
  val engine = EngineWithTrouble() // 特定の条件で失敗するEngineのテスト。テストが終わったらもとに戻すこと
}

テスト目的以外でも、Engineが抽象クラスで、普通のエンジン、水素エンジンを使い分けたいってこともあるかもしれない。これも Carのコードを修正することになる。

class Car {
  // val engine = Engine()
  val engine = HydrogenEngine()
}

まあ、いちいち Carのコードをいじってるのはなんか落ち着きがない。

それでいいじゃんって割り切ってしまうのもありだろうけど、Carのコードには手を加えず他のところから「この Engineを使え」って指示を受ける方が見通しが良くならない? という考えもわかる。「使うエンジンの種類を知りたければ指示書を確認してくれ」というのは筋が通っている。

自分のような古い人間には「元のコードにベタで書いてるほうが間違いないでしょう...そんなにコード量も増えないよ?」という引っかかりがとても強い。 けど、「テストコードを間違えて混入してしまう」という問題にはベタ書きでは対処できないのは理解できる。

Dependency Injection

上記の問題をどう解決するかというと、方法は2つ。

  • Carのコンストラクタで Engineを渡す
  • 直接代入ではない別の方法で engineフィールドに Engineをセットする

前者を「コンストラクタインジェクション」、後者を「フィールドインジェクション」という。

これらを総称して「依存性の注入 (Dependency Injection)」という名前が付けられている。

コンストラクタインジェクションはこう

class Car(private val engine: Engine) {
}

フィールドインジェクションはこう

class Car {
  private val engine = <Engineのインスタンスをどこかで作り、ここに挿入する>
}

いずれにしても、「Engineは Carの外で作る」という前提がある。

となると、次に問題となるのは

  • 誰が Engineを作るのか?
  • どうやってそれを Carに渡すのか?

ということ。

Hiltという Dependency Injectionライブラリ

Androidには Hiltという Dependency Injectionのライブラリが(かなり前に)追加された。

Hiltは Daggerという Dependency Injectionのライブラリを土台にして作られている。 Daggerは結構前から使われていて安定性も良く人気があるが、理解するハードルが高く 「勉強する手間の割に得られる利益が少ない」というものだった。少なくとも自分には。

Hiltはそんな Daggerの難しいところを、使う上での決まり事をいくつか増やすことで比較的簡単に使えるようにしたもの、と理解している。

Hiltを使うと、先程の「どうやってそれを Carに渡すのか?」という問題は以下のように解決できる。

「ここにオブジェクトを入れて欲しい」という箇所に @Inject という修飾子を付けるだけ。

class Car @Inject constructor (private val engine: Engine) {
}
class Car {
  @Inject val engine : Engine
}

これで問題の半分は解決した。あとは「誰が Engineを作るのか?」ということだが、これは Engineの生成方法次第で、方法がいくつかに分かれている。

  • ソースコードが全部自分の管理下にある場合
  • コードは管理下にあるけど、classでなく interfaceのサブクラスを提供したい場合
  • 他の人が作ったライブラリのオブジェクトを提供したい場合

ソースコードが全部自分の管理下にある場合

コンストラクタに Injectアノテーションを付ける。

class Engine @Inject constructor() { ... }

interfaceのサブクラスを提供したい場合

Engineが interfaceの場合、コンストラクタが存在しないので上記の方法が使えない。

interface Engine { ... }   // ← constructorを定義できない
class GasolineEngine : Engine { ... }
class HydrogenEngine : Engine { ... }

こういうときは、オブジェクトの作り方を指示する モジュール を作る。
モジュールはオブジェクトをどう作るかの指示書みたいなもの。

interface Engine { ... }
class GasolineEngine @Inject constructor() : Engine { ... }
class HydrogenEngine @Inject constructor() : Engine { ... }

@InstallIn(SingletonComponent::class)
@Module
abstract interface EngineModule {
  @Binds
  abstract fun bindEngine(engine : HydrogenEngine) : Engine
}

@Module を付けた abstract classを宣言する」「要求される型、それに対して返す実装の型の組み合わせを @Binds で定義する」が鍵。

@Binds はModuleの中にいくつあってもいい。

@InstallIn は「このモジュールをインストールするコンポーネント」だが、これは後述する。SingletonComponentについても後で。

  • Engineの生成に引数が必要なときは? → 後述
  • Engineの生成にcontextが必要なときは? → 後述

他の人が作ったライブラリのオブジェクトを提供したい場合

Engine自体が外で作られたライブラリだと、GasolineEngine、HydrogenEngine のコンストラクタ宣言に Injectを追加できない。こういうときは、モジュールのオブジェクト を作ることで対応する。

@InstallIn(SingletonComponent::class)
@Module
object EngineModule {
  @Provides
  fun provideEngine() = HydrogenEngine()
}

@Provides はModuleの中にいくつあってもいい。

Engineの生成に引数が必要なんだけど?

Engineの生成に他のオブジェクト(以下の場合 Plug)が必要という場合は普通にある。

class Engine (val plug : Plug) { ... }

その場合、必要なクラスも @Injectの対象にする。

コンストラクタを定義できるなら、こう

class Engine @Inject constructor(val plug : Plug) { ... }
class Plug @Inject constructor() { ... }

Plugが interfaceなら、こう

interface Plug { ... }
class PlugImpl @Inject constructor() : Plug { ... }

@InstallIn(SingletonComponent::class)
@Module
abstract interface EngineModule {

  @Binds
  abstract fun bindEngine(engine : GasolineEngine) : Engine

  @Binds
  abstract fun bindPlug(plug : PlugImpl) : Plug
}

上記の例は EngineModuleに書いたが、PlugModuleを新たに定義してもいい。 ただ分割しすぎても見通しが悪くなるので、モジュールの設計はチームで一貫性をもたせるようにしたほうがいい。

Engineの生成にcontextが必要なんだけど?

@ApplicationContext アノテーションを使う。
@ActivityContextというアノテーションもある。必要な方を。

interface Engine { ... }
class GasolineEngine @Inject constructor(@ApplicationContext appContext: Context) : Engine { ... }
class HydrogenEngine @Inject constructor(@ApplicationContext appContext: Context) : Engine { ... }

とか、

@Module
object EngineModule {
  @Provides
  fun provideEngine(@ApplicationContext appContext: Context) {
    return Engine(appContext)
  }
}

とか。

コンポーネント

オブジェクト生成方法の指示書であるモジュールは、必ず コンポーネント に所属する、というルールがある。コンポーネントは何種類かあり、どのコンポーネントに所属させるかを指定するのが @InstallIn アノテーション。

よく使いそうなのは以下の通り

  • SingletonComponent ... Applicationのサブクラス。アプリケーションの生成時から最後まで存在する。
  • ActivityComponent ... アクティビティのライフサイクルと同期するコンポーネント
  • ActivityRetainedComponent ... ViewModelのライフサイクルと同期するコンポーネント
  • FragmentComponent ... Fragmentのライフサイクルと同期するコンポーネント

完全なリスト → https://developer.android.com/training/dependency-injection/hilt-android#generated-components

スコープ

Hiltは基本的に要求があれば指示書を元にオブジェクトを生成するが、アプリケーションで唯一のオブジェクト(要するにシングルトン)が欲しいんだけど、という場合がある。そういうときはスコープを調整する。

以下の例では、 @Singleton アノテーションを使うことで、 アプリケーション中で Engineオブジェクトは一個だけになり、必要とされる場所で同じオブジェクトが使われる。

@InstallIn(SingletonComponent::class)
@Module
object EngineModule {
  @Singleton
  @Provides
  fun provideEngine() = Engine()
}

ActivityScoped, FragmentScopedなどもあるらしい (使ったことないから知らない)。多分おなじアクティビティ(フラグメント)で要求されたら、同じオブジェクトを返してくれるのだと思われる。

https://developer.android.com/training/dependency-injection/hilt-android#component-scopes

GasolineEngineも HydrogenEngineも提供したいんだけど

識別子を定義して、モジュールと使用箇所でそれを指定する。

https://developer.android.com/codelabs/android-hilt#8

仕上げ : エントリポイント

Hiltのルールとして、HiltAndroidApp アノテーションを付けた Applicationのサブクラスを持つ必要がある。 manifestへの登録も忘れずに。

@HiltAndroidApp
class MyApplication : Application() { ... }

最後に、依存性を注入したいクラスにエントリポイント @AndroidEntryPoint もしくは @HiltViewModel を設定する。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }

@AndroidEntryPoint
class MainFragment : Fragment() { ... }

@HiltViewModel
class MainViewModel : ViewModel() { ... }

その他

テストの方法はどうするの? とか、Hilt でサポートされていないクラスに依存関係を注入する必要もある、といった場合もあるが割愛する。そのへんは参考文献を見てほしい。

参考文献

21
16
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
21
16