😁

ゆめみ社のAndroidインターンに参加してきたので学んだことをまとめておく(DI編)

2022/09/07に公開

ゆめみ社のAndroidインターンに参加しました。
なかなかにありがたい経験をさせてもらえたので、アウトプットします👍

  • 基本的には課題を進めていく🚶
  • 課題はAndroidViewで開発する前提になっていたが、Jetpack Composeの方が得意という旨を伝えると、Composeを使ってUIを作っていくことに。柔軟に対応していただくことができた。感謝😊

DI

参考:
めっちゃ良い記事1
https://zenn.dev/itakahiro/articles/e166afcfa8ba40
めっちゃ良い記事2
https://qiita.com/ritukiii/items/de30b2d944109521298f
めっちゃいい記事3
https://android-developers-jp.googleblog.com/2021/10/introduction-to-hilt-in-the-mad-skills-series.html?m=1
親の顔より見た公式ドキュメント
https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

依存性の注入?依存性ってなんのこと?
めっちゃ良い記事2に書いてありました。

「Dependency」(依存性と訳していた)は、「オブジェクト」です。

つまり、DIとは、「依存性の注入」じゃなくて、「オブジェクトの注入」だった訳ですね。

DIしないと...
DIしないと
// 本当に欲しいのはAクラスのオブジェクトa
val d = D()
val c = C(d)
val b = B(c,d)
val a = A(b,d)

a.doSomething()
DIすると...
DIすると
// 事前に別ファイルでいい感じに定義しておくことで以下のようにかける
val a = A()

a.doSomething()

DIライブラリがAクラスのコンストラクタにbやdを自動的に渡してくれるような動き(=オブジェクトb・dを注入)をします。
DIしたほうがコードが見やすくなりますよね!
また、

DIするときでも引数を渡せる
// 事前に定義したインスタンスと違うインスタンスを渡したい場合はそれを引数に渡せばいい
val b = FakeB()
val d = FakeD()

val a = A(b,d)

a.doSomething()

あとはどうやって事前に別ファイルでいい感じに定義をするかを学べばDIを使ってコードを見やすくできます。

AndroidのDI

AndroidではDuggerHiltを使ってDIするのが多いそうです。

https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

Hiltでは次のような手順でDIを行えます。

  1. 事前のセットアップ(@HiltAndroidApp,@AndroidEntryPointなど)
  2. DIしたいクラス(先ほどの例でいうAクラス)のコンストラクタに@Injectをつける
@Injectでこのクラスに依存関係を注入したいということをHiltに伝える
class A @Inject constructor(
  private val b :B, 
  private val d :D,
){
  ...
}

上記の場合Aクラスのコンストラクタに@Injectをつけたのでその引数であるB,Dクラスのインスタンスはわざわざ渡さなくともHiltが自動的に作ってくれるようになります。

  1. Hiltモジュールを定義し、各クラスをどのように生成すればいいのかをHiltに伝える

2までだとA()が呼ばれたときにB・Dクラスをインスタンス化してそれをAの引数に渡すといった処理をHiltが自動的に行なってくれますが、HiltはB・Dクラスをどうインスタンス化すれば良いか分かりません。よってどのクラスを要求されたら何を渡せばいいのかをHiltに教えます。

Hiltモジュールの定義
@Module  // Hiltモジュールであることを宣言
@InstallIn(ActivityComponent::class)  // 伏線1
abstract class XXXModule {

  // Bクラスのインスタンスを要求されたらC,Dクラスから生成する
  @Binds
  abstract fun bindB(    // メソッド名はなんでもいい
    c:C,
    d:D,
  ): B

  // Cクラスのインスタンスを要求されたらDクラスから生成する
  @Binds
  abstract fun bindC(    // メソッド名はなんでもいい
    d:D,
  ): C

// Dクラスのインスタンスを要求されたらそのままインスタンス化
  @Binds
  abstract fun bindD(    // メソッド名はなんでもいい
  ): D
}

HiltModuleの各メソッドが要求されたクラスとそれを作るために必要なインスタンスの対応(バインディング)になります。例えばbindBの記述ではHiltはBを作りたかったらCとDのインスタンスを生成するといったふうに解釈します。

abstractにすると定義に従って返してくれるみたいですが,実装したメソッドも書くことができます。例えば以下のような感じです。

// Xのインスタンスが欲しかったらXのインスタンスをメソッドの内容で作って返す
@Binds
fun bindX():X{
  return X.createSpecialX()
}

DB・ネットワーク関係のインスタンスなど、こちらでインスタンスの作成方法が定義できないクラスはこの方法で渡せます。

これでHiltは以下の手順でAクラスのコンストラクタのインスタンスb,dを取得してきます。

  1. A()が呼ばれる
  2. Aのコンストラクタには@Injectをつけているのでその引数であるB,Dインスタンスを作ろうとする
  3. BインスタンスについてはHiltModule内でBを作りたかったらC,Dインスタンスを渡せと書いてあるのでC,Dインスタンスを作ろうとする
  4. Cを作ろうとすると...... -> Cインスタンスが作れる
  5. Dを作ろうとすると...... -> Dインスタンスが作れる
  6. C,Dインスタンスが作れたのでBのコンストラクタに渡す -> Bインスタンスが作れる
  7. A()にさっき作ったB,Dインスタンスを渡す -> Aインスタンスが作れる

具体的な方法は公式ドキュメントを参照してください

同じクラスのインスタンスを要求されると何個も作ろうとはしない

例えば

class A @Inject constructor(c:C)
class B @Inject constructor(c:C)
class C()

// Hiltモジュール内
// Cを要求されたら引数なしでインスタンス化
@Singleton
@Provides
fun provideC():C{
    return C()
}

Cを提供するバインディング(provideC)に@Singletonアノテーションを使用しました。

このときAのコンストラクタに渡されるCインスタンスとBのコンストラクタに渡されるCインスタンスは全く同じインスタンスが渡されます。つまり@Singletonアノテーションを使用するとHiltは要求されたクラスのインスタンスが既に作成済みの場合は新しくインスタンス化せずに既に作ってあるものを渡そうとするわけです。何度もインスタンス化しない分メモリを節約できるというわけです。(またそのインスタンスはシングルトンなインスタンスになります)

InstallInとは? (伏線1回収)

ところで(@Moduleを使って)Hiltモジュールを宣言したときに一緒に@InstallIn()というアノテーションを使っていましたが、これはいったいなんでしょう?

@InstallIn(◯◯) のカッコ内にはHiltコンポーネントを指定します。

HiltコンポーネントとはHiltがインスタンスを提供するために値をおぼておくために使用するインスタンスの入れ物です。

今までの状況からすると準備をすればHiltは自動的にインスタンスを保持しそれを利用時に渡してくれる役割を担っています。これを図に示すと以下の通りです。

ですが実際は自動的にインスタンスを保持してくれるのはHilt自身ではなくその中のHiltコンポーネントと呼ばれる入れ物の中です

このコンポーネントと呼ばれる入れ物はActivityやViewModelのようなAndroidフレームワークのクラスに紐づいていて、これらのクラス(のインスタンス)がメモリ上から解放されるとコンポーネント内のインスタンスも一緒に解放されます

Activity内でしか使わないものはActivityComponent内に色々なインスタンスを保持し、ViewModel内で使うものはViewModelComponent内にインスタンスを保持すれば、メモリの節約になるわけです。こういった管理もいい感じにしてくれるのがHiltです。

以上より、@InstallIn(XXComponent::class)はXXComopnentにHiltモジュールをインストールする(追加する)という意味になります。対応するコンポーネントに紐付けさせれば。(めんどくさかったら全部SingletonComponentでいいや)

ちなみにHiltコンポーネントとAndroidクラスの対応はドキュメント内にあります。

まとめ

Hiltの各用語を図示すると以下のようになります。

インターンアウトプットシリーズ一覧

Discussion