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()
}
}
この例では、UserServiceはUserRepositoryに強く依存しています。
この構造が抱える問題点
一見シンプルですが、以下の問題を内包しています。
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時間、マルチモジュール構成などで最適解は変わります。
学習ステップのおすすめ
- 手動DIで仕組みを理解
- インターフェースで疎結合化
- テストでDIの効果を体感
- DIライブラリを導入
まとめ
- DIは「設計を健全に保つための技術」
- KotlinはDIと非常に相性が良い
- まずはコンストラクタインジェクションを徹底
- ライブラリ選定は規模と目的で判断
以上、KotlinのDI(依存性注入)についてでした。
最後までお読みいただき、ありがとうございました。










