ゆめみ社のAndroidインターンに参加してきたので学んだことをまとめておく(DI編)
ゆめみ社のAndroidインターンに参加しました。
なかなかにありがたい経験をさせてもらえたので、アウトプットします👍
- 基本的には課題を進めていく🚶
- 課題はAndroidViewで開発する前提になっていたが、Jetpack Composeの方が得意という旨を伝えると、Composeを使ってUIを作っていくことに。柔軟に対応していただくことができた。感謝😊
DI
参考:
めっちゃ良い記事1
めっちゃ良い記事2
めっちゃいい記事3
親の顔より見た公式ドキュメント
依存性の注入?依存性ってなんのこと?
めっちゃ良い記事2に書いてありました。
「Dependency」(依存性と訳していた)は、「オブジェクト」です。
つまり、DIとは、「依存性の注入」じゃなくて、「オブジェクトの注入」だった訳ですね。
例
DI
しないと...
// 本当に欲しいのはAクラスのオブジェクトa
val d = D()
val c = C(d)
val b = B(c,d)
val a = A(b,d)
a.doSomething()
DI
すると...
// 事前に別ファイルでいい感じに定義しておくことで以下のようにかける
val a = A()
a.doSomething()
DIライブラリがAクラスのコンストラクタにbやdを自動的に渡してくれるような動き(=オブジェクトb・dを注入)をします。
DIしたほうがコードが見やすくなりますよね!
また、
// 事前に定義したインスタンスと違うインスタンスを渡したい場合はそれを引数に渡せばいい
val b = FakeB()
val d = FakeD()
val a = A(b,d)
a.doSomething()
あとはどうやって事前に別ファイルでいい感じに定義
をするかを学べばDIを使ってコードを見やすくできます。
AndroidのDI
AndroidではDuggerHiltを使ってDIするのが多いそうです。
Hiltでは次のような手順でDIを行えます。
- 事前のセットアップ(@HiltAndroidApp,@AndroidEntryPointなど)
- DIしたいクラス(先ほどの例でいうAクラス)のコンストラクタに@Injectをつける
class A @Inject constructor(
private val b :B,
private val d :D,
){
...
}
上記の場合Aクラスのコンストラクタに@Injectをつけたのでその引数であるB,Dクラスのインスタンスはわざわざ渡さなくともHiltが自動的に作ってくれるようになります。
- Hiltモジュールを定義し、各クラスをどのように生成すればいいのかをHiltに伝える
2までだとA()
が呼ばれたときにB・Dクラスをインスタンス化してそれをAの引数に渡すといった処理をHiltが自動的に行なってくれますが、HiltはB・Dクラスをどうインスタンス化すれば良いか分かりません。よってどのクラスを要求されたら何を渡せばいいのかを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を取得してきます。
-
A()
が呼ばれる - Aのコンストラクタには@Injectをつけているのでその引数である
B,Dインスタンス
を作ろうとする - BインスタンスについてはHiltModule内でBを作りたかったらC,Dインスタンスを渡せと書いてあるので
C,Dインスタンス
を作ろうとする - Cを作ろうとすると...... ->
Cインスタンス
が作れる - Dを作ろうとすると...... ->
Dインスタンス
が作れる -
C,Dインスタンス
が作れたのでBのコンストラクタに渡す ->Bインスタンス
が作れる -
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