KotlinのDI(依存性注入)について

採用はこちら

KotlinにおけるDI(Dependency Injection/依存性注入)は、設計の健全性・保守性・テスト容易性を大きく左右する重要な概念です。

特にAndroid開発、Ktorを用いたサーバーサイド開発、近年ではCompose Multiplatformなどでも、DIの理解はほぼ必須になっています。

この記事では、

  • DIの基本概念
  • なぜKotlinでDIが重要なのか
  • Kotlinにおける代表的なDI実装パターン
  • 主要DIライブラリ(Hilt / Koin)
  • 実務目線での選択基準と注意点

までを体系的に解説します。

目次

DI(依存性注入)とは何か

「依存性」とは

あるクラスが、別のクラスを内部で直接生成・保持している状態を「依存している」と言います。

class UserService {
    private val repository = UserRepository()

    fun getUser() {
        repository.fetch()
    }
}

この例では、UserServiceUserRepository強く依存しています。

この構造が抱える問題点

一見シンプルですが、以下の問題を内包しています。

  • UserRepositoryを差し替えられない
  • テスト時にFakeやMockを注入しづらい
  • 実装変更の影響範囲が広がりやすい
  • クラスの責務が肥大化しやすい

結果として、変更に弱く、テストしづらいコードになりがちです。

DIの基本思想

DIの考え方は非常にシンプルです。

依存するオブジェクトは、自分で生成せず、外部から渡してもらう

class UserService(
    private val repository: UserRepository
) {
    fun getUser() {
        repository.fetch()
    }
}

これが 依存性注入(Dependency Injection) です。

DIは、IoC(Inversion of Control:制御の反転)を実現する代表的な手段でもあります。

生成や配線の責務を利用側から切り離すことで、設計の柔軟性が高まります。

KotlinでDIが重要な理由

Kotlinは以下の特徴を持ちます。

  • コンストラクタ定義が簡潔
  • valによる不変設計が自然
  • null安全
  • インターフェース・関数型設計との親和性が高い

これらはDIと非常に相性が良く、DIを前提とした健全な設計を書きやすい言語と言えます。

Kotlinにおける基本的なDIパターン

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

最も推奨されるDI手法です。

class UserViewModel(
    private val userService: UserService
)

メリット

  • 依存関係が明示される
  • 不変性を保てる
  • テストが容易
  • ライブラリDIとも自然に接続できる

Kotlinでは、まずこれを徹底するのが基本です。

セッターインジェクション

class UserService {
    lateinit var repository: UserRepository
}

後から依存を注入する方法ですが、

  • 未初期化リスクがある
  • 不変性が壊れる

といった理由から、Kotlinではあまり推奨されません。

インターフェースによる疎結合

DIとセットで使われる重要な考え方です。

interface UserRepository {
    fun fetch()
}

class UserRepositoryImpl : UserRepository {
    override fun fetch() {}
}

実装を隠蔽し、差し替え可能な設計を実現します。

DIを導入するメリット

テストが書きやすくなる

class FakeUserRepository : UserRepository {
    override fun fetch() = "test user"
}

val service = UserService(FakeUserRepository())
  • Fake実装を簡単に注入できる
  • 高速で安定したテストが書ける
  • モックフレームワークに依存しない設計も可能

※ ただし、ケースによっては Mockito / MockK 等を併用することも現実的です。

責務が明確になる

  • UI / ViewModel / UseCase / Repository の分離
  • Clean Architectureとの高い親和性
  • 変更に強い構造

Kotlin主要DIライブラリ

Dagger / Hilt(Androidの主流)

特徴

  • コンパイル時DI
  • 高速・型安全
  • 学習コストは高め
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel()

※ 実際の動作には以下が必要です。

  • @HiltAndroidApp を付与した Application
  • @AndroidEntryPoint を付与した Activity / Fragment / Compose
  • Repositoryの提供定義(@Binds / @Provides

大規模・長期運用向き
小規模にはオーバースペックな場合も

Koin(KotlinらしいDSLベースDI)

特徴

  • DSLで定義
  • 実行時(ランタイム)に依存解決
  • 学習コストが低い
val appModule = module {
    single<UserRepository> { UserRepositoryImpl() }
    viewModel { UserViewModel(get()) }
}

小〜中規模
学習・個人開発
起動時・解決時にオーバーヘッドが出る場合がある

Manual DI(ライブラリ不使用)

class AppContainer {
    val repository = UserRepositoryImpl()
    val service = UserService(repository)
}
  • 依存関係が明確
  • シンプルで透明性が高い
  • 規模が大きくなると管理が煩雑

用途と規模次第では、現在でも十分実用的です。

Android × Kotlin における代表的なDI構成例

class UserViewModel(
    private val getUserUseCase: GetUserUseCase
)

class GetUserUseCase(
    private val repository: UserRepository
)

class UserRepositoryImpl(
    private val api: UserApi
)
  • UI → UseCase → Repository → DataSource
  • 責務分離が明確
  • テストしやすい構造

よくあるDIアンチパターン

Service Locator

RepositoryProvider.get()
  • 依存関係が見えづらい
  • テストが困難になりやすい

※ 小規模や特殊用途で使われることもありますが、長期運用・大規模開発では避けられる傾向があります。

実務目線での選択指針

規模・状況推奨
学習・小規模Manual DI / Koin
中規模Koin / Hilt
大規模・企業Hilt(Dagger)

※ チームの習熟度、CI時間、マルチモジュール構成などで最適解は変わります。

学習ステップのおすすめ

  1. 手動DIで仕組みを理解
  2. インターフェースで疎結合化
  3. テストでDIの効果を体感
  4. DIライブラリを導入

まとめ

  • DIは「設計を健全に保つための技術」
  • KotlinはDIと非常に相性が良い
  • まずはコンストラクタインジェクションを徹底
  • ライブラリ選定は規模と目的で判断

以上、KotlinのDI(依存性注入)についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次