Android / Kotlin / Programming

Android Hiltってなに?

最初、Hiltが何なのか皆目見当もつきませんでしたが公式文章をちゃんと読んでみたら何となくわかったので、依存関係インジェクションからHiltまでの説明をまとめました。

ざっくり結論

依存関係インジェクション(Dependency Injection)を手作業より簡単にできるライブラリ

的なことを公式には書いてますが、手動での依存関係インジェクション管理すらしたことがなかったのでよくわかりませんでした。

ここから先は
・依存関係インジェクションの説明
・手動による依存関係インジェクション
 ・基本と問題点
 ・コンテナを使用した管理と問題点
・Hiltを使用して依存関係インジェクション
を説明していきます。

依存関係インジェクション(Dependency Injection)

まずはこのコードを見てください。

fun main() {
    val car = Car()
    car.startEngine() // Gasoline engine starts
}

class Car {
    private val engine = GasolineEngine()
    
    fun startEngine() {
        engine.start()
    }
}

class GasolineEngine {
    fun start() {
        println("Gasoline engine starts")
    }
}

CarクラスとEngineクラスがあり、CarクラスのstartEngine()関数でGasolineEngineクラスのstart()関数を呼び出しています。

このコードではGasolineEngineクラスをCarクラス内で作成しています。
このままだとGasolineEngineクラスをElectricEngineクラスなどの別のクラスに変えることができないため、下のコードのように新しいCarクラスを作成する必要があります。

fun main() {
    val car = Car()
    car.startEngine() // Gasoline engine starts
    
    val electricCar = ElectricCar()
    electricCar.startEngine() // Electric engine starts
}

/**
 * Carクラス
 */
class Car {
    private val engine = GasolineEngine()
    
    fun startEngine() {
        engine.start()
    }
}

class GasolineEngine {
    fun start() {
        println("Gasoline engine starts")
    }
}

/**
 * Carクラスとほぼ同じElectricCarクラス
 */
class ElectricCar {
    private val engine = ElectricEngine()
    
    fun startEngine() {
        engine.start()
    }
}

class ElectricEngine {
    fun start() {
        println("Electric engine starts")
    }
}

しかし下記のように各Engineクラスが共通のインターフェースもしくはクラスを継承し、Carクラスのコンストラクタで継承元を型として受け取るようにすれば避けることができます。

fun main() {
    val gasolineEngine = GasolineEngine()
    val car = Car(gasolineEngine)
    car.startEngine() // Gasoline engine starts
    
    val electricEngine = ElectricEngine()
    val electricCar = Car(electricEngine)
    electricCar.startEngine() // Electric engine starts
}

class Car(
    private val engine: Engine
) {
    fun startEngine() {
        engine.start()
    }
}

class GasolineEngine: Engine {
    override fun start() {
        println("Gasoline engine starts")
    }
}

class ElectricEngine: Engine {
    override fun start() {
        println("Electric engine starts")
    }
}

interface Engine {
    fun start()
}

GasolineEngineとElectricEngineクラスが継承するEngineインターフェースを定義します。
次にGasolineEngineとElectricEngineクラスにEngineインターフェースを継承させ、start()関数をオーバーライドします。
Carクラスでは内部でEngineを作成する部分を消去し、コンストラクタのパラメータにEngine型の変数を設定します。

このように内部でクラスをインスタンス化するのではなく、外部から受け取る方法を依存関係インジェクション(依存関係挿入)と呼びます。

手動による依存関係インジェクション

基本

公式の文章に沿って説明します。
まず下の図はフローを表しています。

ここで重要なのはUserRepository、UserLocalDataSource、UserReomoteDataSourceです。
UserRepositoryからフローの矢印がそれぞれに伸びているため、UserRepositoryの中でUserLocalDataSource、UserReomoteDataSourceの操作を行うことを意味しています。

Android公式 依存関係挿入 手動による依存関係挿入 手動依存関係挿入の基本より

よって依存関係インジェクションを考慮したコードはこのようになります。

class UserLocalDataSource { ... }
class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Android公式 依存関係挿入 手動による依存関係挿入 手動依存関係挿入の基本より

UserRepositoryクラスの中でUserLocalDataSource、UserRemoteDataSourceクラスをインスタンス化すると依存関係が強くなるため、コンストラクタにそれぞれの型のパラメータを設定することで依存関係インジェクションを行っています。

LoginActivityはコードの開始位置です。
LoginViewModelはUserRepositoryに矢印が伸びているので、コンストラクタでUserRepository型が設定されています。
Retrofitは今回の内容に直接は関係しないため、こんな感じで使用するんだな程度で大丈夫です。
UserRemoteDataSourceからRetrofitに矢印が伸びているのでUserRemoteDataSourceのコンストラクタにはRetrofit型が設定されています。

次のコードはLoginViewModelの引数にUserRepository型を渡すまでの過程です。

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()
        
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        loginViewModel = LoginViewModel(userRepository)
    }
}

Android公式 依存関係挿入 手動による依存関係挿入 手動依存関係挿入の基本より

このコードはloginViewModel = LoginViewModel(userRepository)をゴールとしています。
・LoginViewModelをインスタンス化するにはUserRepositoryが必要
・UserRepositoryをインスタンス化するにはUserRemoteDataSourceとUserLocalDataSourceが必要
・UserRemoteDataSourceをインスタンス化するにはRetrofitが必要

問題点

このようにLoginViewModelクラスをインスタンス化するだけで多数の他クラスをこの順番でインスタンス化する必要があります(UserRemoteDataSourceとUserLocalDataSourceは順不同)。
また、他の場所でLoginViewModelクラスをインスタンス化する場合でも同じコードを複製する必要があります。(ボイラープレートコードが増えるともいいます。)

これらを解決する方法としてコンテナを使用する方法があります。

コンテナを使用した管理と問題点

コンテナ?

上記のLoginViewModelクラスなどのように、対象クラスのインスタンス化までに必要な他クラスのインスタンス化を全て行い、管理するクラスです。

こちらのコードがコンテナと呼ばれるものです。

class AppContainer {

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

Android公式 依存関係挿入 手動による依存関係挿入 コンテナを使用した依存関係の管理より

AppContainerクラス(コンテナ)の役割はLoginViewModelクラスのインスタンス化に必要なUserRepositoryクラスまでのインスタンス化とUserRepositoryクラスの公開です。

コンテナは全てのActivityで使用されるため、下記のように配置することでuserRepository変数にアクセス可能になります。

class AppContainer {
    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

class MyApplication : Application() {
    val appContainer = AppContainer()
}

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)
    }
}

Android公式 依存関係挿入 手動による依存関係挿入 コンテナを使用した依存関係の管理より

この方法でボイラープレートコードを減らすことは可能ですがまだまだ問題点があります。

問題点

コンテナの管理を手動でしなければならないという問題があります。
プロジェクトが大きければ大きいほど、修正を加えれば加えるほどコンテナの修正が発生する可能性が高く、手動で管理を行うと漏れや実装ミスなど考えられます。

場合によってはコンテナの破棄を適切なタイミングやアプリのライフサイクルに沿って実装する必要があります。
公式の例にもあるように、ログインをするフローでコンテナを扱う際にフロー開始時にコンテナを作成し、フロー終了時に破棄するなどが必要になります。

このようにコンテナを使用しても修正や管理が必要になります。
しかしHiltを使えばこれらを簡単に行えるようです。
次の記事ではHiltの使い方をまとめようと思います。

コメント

コメントを残す

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