[Android] Hiltを勉強したのでまとめる

Hiltを完全に理解したのでメモしておきます.

概要

HiltはAndroidでDependency Injection(DI, 依存性インジェクション)を行うためのライブラリ.

Dependency Injection is 何

例えばこんなコードがあるとする.

class TaskList{
    private val taskDatabase = Database()
    fun add(taskTitle: String){...}
}
fun main(){
    val taskList = TaskList()
    taskList.add("foo")
}

TaskListDatabaseに依存している.
TaskListを使いまわしたい時,taskDatabaseがハードコードされているので他のDatabaseを使えなくなってしまう.これでは不便.

コンストラクタで依存関係を受け取ることでこの問題を解決できる.こういうパターンがDependency Injectionと呼ばれる
(コンストラクタで依存関係を受け取っているため,特にConstruction Injectionと呼ばれる).

class TaskList(private val taskDatabase: Database){
    fun add(taskTitle: String){...}
}
fun main(){
    private val database = Database()
    val taskList = TaskList(database)
    taskList.add("foo")
}

なぜHilt?

Construction InjectionをActivityでも行いたい.
しかし,Activityのインスタンスはシステム側で自動生成されてしまうため,引数が必要なコンストラクタを定義することができない.
そこでHiltを用いてDIを行う.

つかいかた

下準備

プロジェクトのbuild.gradleを編集.

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.30.1-alpha'
    }
}

app/build.gradleを編集.

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.30.1-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.30.1-alpha"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    // When using Kotlin.
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
    // When using Java.
    annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}

Applicationクラスに@HiltAndroidAppをつける.

@HiltAndroidApp
class TodoApplication:Application() {}

Activity・FragmentでDIを行う

クラスに@AndroidEntryPointをつけて,変数に@Injectをつけるだけで良い.

@AndroidEntryPoint
class MainActivity: AppCompatActivity(){
    @Inject lateinit var taskList: TaskList
    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        taskList.add("bar")
  }
}

HiltがTaskListの作り方を知っている場合,MainActivityが作られた時にtaskListの実体が生成される(taskListがInjectされる).

コンストラクタをいじらなくてもDIできそう.やったね!

Hiltにインスタンス生成方法を教える

「ちょっと待て,じゃあどうやってHiltにTaskListの作り方を教えるんだ?」という話.

自分でコンストラクタを定義できる場合(基本)

コンストラクタを自分でいじれる場合は@Injectアノテーションをコンストラクタにつけるだけ.

class TaskList @Inject constructor(private val taskDatabase: Database){...}

TaskListのインスタンスが必要になった時,HiltはDatabaseを使ってインスタンスを提供する.
Databaseの作り方がわからない場合はコンパイルエラーになるので,別の方法でHiltに教える必要がある.

自分でコンストラクタを定義できる場合(ViewModel編)

ViewModelの場合は@ViewModelInjectアノテーションをコンストラクタにつける.
SavedStateHandleを受け取る際に@Assistedアノテーションをつける.

class TaskListViewModel @ViewModelInject constructor
(@Assisted private val savedStateHandle: SavedStateHandle,
 private val taskRepository: TaskRepository){...}

自分でコンストラクタを定義できない場合(外部ライブラリ編)

外部ライブラリを使いたいときは@Module@InstallIn@Providesを使って次のように書く.

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {
    @Provides
    fun provideTaskDatabase(app: Application) =
        Room.databaseBuilder(
            app,
            Database::class.java,
            "task_db"
        ).build()

    // 複数の関数を定義可能
}

@Providesアノテーションをつけた関数は,次の情報をHiltに渡す.

  • 「関数の戻り値の型」は「関数の戻り値」でInjectできる.
  • 「関数の戻り値の型」は「関数の引数」に依存する.

ApplicationクラスはHiltが自動で依存関係をInjectしてくれる.詳しくはコンポーネントのデフォルトバインディングを参照.

@Moduleは関数をまとめた”Module”を作るためのおまじない.
@InstallInは後述.

自分でコンストラクタを定義できない場合(インターフェース編)

インターフェースのインスタンスをInjectしたい場合は@Module@InstallIn@Bindsを使って次のように書く.

interface TaskInterface{...}

class TaskInterfaceImpl @Inject constructor(...):TaskInterface{...}

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule{
    @Binds
    abstract fun bindTaskInterface(taskInterfaceImpl: TaskInterfaceImpl)
    :TaskInterface
}

@Bindsアノテーションをつけた関数は,次の情報をHiltに渡す.

  • 「関数の戻り値の型のインターフェース」は「関数の引数」でInjectできる.

ライフサイクルの設定

先程端折った@InstallInに関わる.
@InstallIn(SingletonComponent::class)と書いた場合,そのModuleのメソッドはSingletonComponentにインストールされ,ApplicationをInjectの対象にするようになる?(ここらへんはあまり自信ないです)
インスタンスの寿命はApplicationと同じになる.
他にもActivityComponentFragmentComponentなどがある.詳しくはAndroid クラスに対して生成されたコンポーネントを参照.

スコープの設定

デフォルトでは,Hiltはその型が必要とされるたびに新しいインスタンスを作成する.
これを避けて1つのインスタンスを共有したい場合はスコープを設定する.
@InstallIn(SingletonComponent::class)と書いたModuleの場合,@Singletonアノテーションをつけることで,1つのインスタンスがApplication全体で使い回される.

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Singleton
    @Provides
    fun provideTaskDatabase(app: Application){...}
}

他にも@ActivityScoped@FragmentScopedなどがある.

@InstallInで指定したクラスとスコープは1対1で対応していて,クラスと対応していないスコープを使用することはできない.詳しくはComponent hierarchyを参照.

その他

公式ドキュメント含めてSingletonComponentの代わりにApplicationComponentが使われている資料が多い.これはDagger 2.30でApplicationComponentが改名された影響らしい.
ApplicationComponentはそのうち削除されるとアナウンスがあるので,SingletonComponentを使うべし.
(参考: GitHub )

おわりに

Hiltチョットデキルようになったら書き直したいと思います.

参考資料

コメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA