Hilt/KoinとViewModelの連携:Kotlinでの依存性注入 詳細解説
ViewModelは、AndroidアプリのUIロジックをUI Controller(ActivityやFragment)から分離するための重要なコンポーネントです。UIのライフサイクルに依存せず、データの保持とUIの更新を担当します。しかし、ViewModel自身もデータベースアクセス、ネットワークリクエスト、複雑なビジネスロジックなど、多くの依存関係を持つことがあります。これらの依存関係を効率的かつテストしやすい形で管理するためには、依存性注入(DI)が不可欠です。
本記事では、KotlinでViewModelと依存性注入ライブラリHiltとKoinを連携させる方法を詳細に解説します。それぞれのライブラリの基本的な概念、ViewModelとの連携方法、利点と欠点、そして具体的なコード例を通じて、実践的な知識を習得できるように構成されています。
目次
- はじめに:なぜViewModelに依存性注入が必要なのか
- ViewModelの役割と重要性
- 依存性注入の基本概念
- ViewModelでDIを利用するメリット
- HiltによるViewModelへの依存性注入
- Hiltの概要と特徴
- Hiltのセットアップ:build.gradleの設定
@HiltViewModel
アノテーションによるViewModelの注入@Provides
アノテーションによる依存性の提供@Inject
アノテーションによる依存性の注入@ViewModelScoped
アノテーション:ViewModelのスコープ管理- ViewModelFactoryの自動生成
- HiltによるViewModelのテスト
- Hiltの利点と欠点
- KoinによるViewModelへの依存性注入
- Koinの概要と特徴
- Koinのセットアップ:build.gradleの設定
viewModel
関数によるViewModelの定義get()
関数による依存性の解決stateViewModel
関数:SavedStateHandleの利用- KoinによるViewModelのテスト
- Koinの利点と欠点
- Hilt vs Koin:どちらを選ぶべきか
- 設計思想の違い
- 学習コストと実装の容易さ
- コンパイル時の安全性と実行時の柔軟性
- パフォーマンスへの影響
- チームの規模とプロジェクトの複雑さ
- 実践的な例:HiltとKoinを用いたViewModelの構築
- シンプルなカウンターアプリの実装(Hilt/Koinそれぞれ)
- ネットワークリクエストを行うViewModelの実装(Hilt/Koinそれぞれ)
- データベースアクセスを行うViewModelの実装(Hilt/Koinそれぞれ)
- ViewModelとStateFlow/LiveDataの連携
- StateFlowとLiveDataの選択
- Hilt/KoinとStateFlow/LiveDataの組み合わせ
- UIへのデータの反映
- まとめ:最適な依存性注入戦略の選択
- プロジェクトの要件に合わせたライブラリの選択
- 依存性注入のベストプラクティス
- 今後の学習への道しるべ
1. はじめに:なぜViewModelに依存性注入が必要なのか
1.1 ViewModelの役割と重要性
Androidアプリ開発において、ViewModelはUI Controller(ActivityやFragment)とデータ層の間を取り持つ役割を担います。具体的には、以下の責任を持ちます。
- データの保持: UIに必要なデータを保持し、画面回転などの設定変更時にもデータを維持します。
- UIのロジック: UIに必要なデータの変換、フォーマット、フィルタリングなど、UIに特化したロジックを処理します。
- データ層へのアクセス: RepositoryやUseCaseなどのデータ層のコンポーネントを呼び出し、必要なデータを取得したり、更新したりします。
- UIへの通知: データが変更された際に、LiveDataやStateFlowなどのオブザーバブルなデータホルダーを通してUIに通知します。
ViewModelを利用することで、UI ControllerはUIの描画とイベント処理に集中でき、よりシンプルでテストしやすいコードになります。また、データのライフサイクル管理をViewModelに委ねることで、メモリリークのリスクを軽減し、アプリの安定性を向上させることができます。
1.2 依存性注入の基本概念
依存性注入(DI)は、オブジェクトが自身の依存関係を自分で生成するのではなく、外部から提供されるようにする設計パターンです。これにより、以下のメリットが得られます。
- 疎結合: オブジェクト間の依存関係が明確になり、変更に強いコードになります。
- テスト容易性: 依存関係をモックに置き換えることで、オブジェクトを単独でテストできます。
- 再利用性: オブジェクトを様々なコンテキストで再利用できます。
- 保守性: コードが整理され、変更や拡張が容易になります。
DIを実現する方法はいくつかありますが、代表的なものとして以下の3つがあります。
- コンストラクタインジェクション: オブジェクトのコンストラクタを通して依存関係を注入します。これが最も推奨される方法です。
- セッターインジェクション: オブジェクトのセッターメソッドを通して依存関係を注入します。
- インターフェースインジェクション: インターフェースを通して依存関係を注入します。
1.3 ViewModelでDIを利用するメリット
ViewModelはしばしば、Repository、UseCase、Database、ネットワーククライアントなど、多くの依存関係を持ちます。これらの依存関係をViewModel内で直接生成すると、以下の問題が発生します。
- 結合度の高さ: ViewModelが具体的な実装に依存するため、テストが難しく、変更に弱いコードになります。
- テストの困難さ: 依存関係をモックに置き換えるのが難しく、単体テストが複雑になります。
- コードの重複: 複数のViewModelで同じ依存関係を生成する場合、コードが重複し、保守性が低下します。
ViewModelにDIを適用することで、これらの問題を解決し、以下のメリットが得られます。
- テスト容易性の向上: 依存関係をモックに置き換えることで、ViewModelを簡単にテストできます。
- 疎結合: ViewModelが具体的な実装から独立するため、変更に強いコードになります。
- 再利用性: ViewModelを様々なコンテキストで再利用できます。
- 保守性の向上: コードが整理され、変更や拡張が容易になります。
2. HiltによるViewModelへの依存性注入
2.1 Hiltの概要と特徴
Hiltは、Googleが提供するAndroidアプリ向けのDIライブラリです。Daggerをベースにしており、Androidフレームワークのコンポーネント(Activity、Fragment、Service、BroadcastReceiver、View、ViewModelなど)への依存性注入を容易にするように設計されています。
Hiltの主な特徴は以下の通りです。
- 標準化されたアノテーション:
@AndroidEntryPoint
,@HiltViewModel
,@InstallIn
,@Provides
,@Inject
などのアノテーションを使用することで、DIの設定を簡潔に記述できます。 - 自動生成されるコード: Hiltはコンパイル時にDIに必要なコードを自動的に生成するため、ボイラープレートコードを削減できます。
- Androidフレームワークとの統合: Activity、Fragment、ServiceなどのAndroidフレームワークのコンポーネントへの依存性注入を容易に行えます。
- テストサポート: Hiltはテスト用のモジュールを提供し、テスト環境での依存関係の置き換えを容易にします。
- ViewModelサポート:
@HiltViewModel
アノテーションを使用することで、ViewModelへの依存性注入を簡単に行えます。
2.2 Hiltのセットアップ:build.gradleの設定
Hiltを使用するためには、まずプロジェクトのbuild.gradle
ファイルに以下の依存関係を追加する必要があります。
プロジェクトレベルのbuild.gradle (build.gradle.kts):
kotlin
plugins {
id("com.android.application") version "..." apply false
id("com.android.library") version "..." apply false
id("org.jetbrains.kotlin.android") version "..." apply false
id("com.google.dagger.hilt.android") version "..." apply false // Hilt Plugin
}
モジュールレベルのbuild.gradle (app/build.gradle.kts):
“`kotlin
plugins {
id(“com.android.application”)
id(“org.jetbrains.kotlin.android”)
id(“kotlin-kapt”)
id(“com.google.dagger.hilt.android”)
}
android {
// …
}
dependencies {
implementation(“com.google.dagger:hilt-android:2.48”)
kapt(“com.google.dagger:hilt-android-compiler:2.48”)
implementation(“androidx.hilt:hilt-navigation-compose:1.2.0”) // ViewModel for NavigationCompose
kapt(“androidx.hilt:hilt-compiler:1.2.0”)
// For instrumentation tests.
androidTestImplementation("com.google.dagger:hilt-android-testing:2.48")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48")
// For local unit tests.
testImplementation("com.google.dagger:hilt-android-testing:2.48")
kaptTest("com.google.dagger:hilt-android-compiler:2.48")
}
kapt {
correctErrorTypes = true
}
“`
上記の例では、Hiltのバージョンを2.48に設定しています。最新のバージョンは、Hiltの公式ドキュメントで確認してください。
また、Hiltを使用するためには、アプリケーションクラスに@HiltAndroidApp
アノテーションを付与する必要があります。
kotlin
@HiltAndroidApp
class MyApplication : Application() {
// ...
}
2.3 @HiltViewModel
アノテーションによるViewModelの注入
HiltでViewModelに依存性注入を行うためには、まずViewModelに@HiltViewModel
アノテーションを付与します。
kotlin
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository,
private val myUseCase: MyUseCase
) : ViewModel() {
// ...
}
@HiltViewModel
アノテーションは、HiltにViewModelを管理させることを指示します。@Inject constructor
は、コンストラクタインジェクションを行うことを意味します。上記の例では、MyViewModel
はMyRepository
とMyUseCase
の依存関係を持っており、これらの依存関係はHiltによって自動的に注入されます。
2.4 @Provides
アノテーションによる依存性の提供
ViewModelが依存するMyRepository
とMyUseCase
は、Hiltに提供される必要があります。これには@Provides
アノテーションを使用します。@Provides
アノテーションは、依存関係を提供するメソッドに付与します。
Hiltでは、モジュールという概念を用いて依存関係を定義します。モジュールは、@Module
アノテーションが付与されたクラスです。
“`kotlin
@Module
@InstallIn(SingletonComponent::class) // or ViewModelComponent::class
object AppModule {
@Provides
@Singleton // or @ViewModelScoped
fun provideMyRepository(impl: MyRepositoryImpl): MyRepository {
return impl
}
@Provides
@Singleton // or @ViewModelScoped
fun provideMyUseCase(myRepository: MyRepository): MyUseCase {
return MyUseCase(myRepository)
}
}
“`
@Module
アノテーションは、このクラスがHiltモジュールであることを示します。@InstallIn
アノテーションは、このモジュールがどのコンポーネントにインストールされるかを指定します。SingletonComponent::class
は、アプリケーションスコープにインストールされることを意味します。ViewModelComponent::class
は、ViewModelのライフサイクルスコープにインストールされることを意味します。@Provides
アノテーションは、このメソッドが依存関係を提供することを示します。@Singleton
アノテーションは、この依存関係がシングルトンであることを示します。@ViewModelScoped
アノテーションは、ViewModelのライフサイクルスコープを持つことを示します。
2.5 @Inject
アノテーションによる依存性の注入
@Inject
アノテーションは、依存関係を注入するフィールドまたはコンストラクタに付与します。前の例で、MyViewModel
のコンストラクタに@Inject
アノテーションを付与しました。
kotlin
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository,
private val myUseCase: MyUseCase
) : ViewModel() {
// ...
}
これにより、HiltはMyRepository
とMyUseCase
のインスタンスを自動的に生成し、MyViewModel
のコンストラクタに注入します。
2.6 @ViewModelScoped
アノテーション:ViewModelのスコープ管理
@ViewModelScoped
アノテーションは、依存関係のライフサイクルをViewModelに紐付けるために使用します。@ViewModelScoped
アノテーションが付与された依存関係は、ViewModelが破棄される際に自動的に破棄されます。
“`kotlin
@Module
@InstallIn(ViewModelComponent::class)
object AppModule {
@Provides
@ViewModelScoped
fun provideMyRepository(impl: MyRepositoryImpl): MyRepository {
return impl
}
@Provides
@ViewModelScoped
fun provideMyUseCase(myRepository: MyRepository): MyUseCase {
return MyUseCase(myRepository)
}
}
“`
上記の例では、MyRepository
とMyUseCase
は@ViewModelScoped
でスコープされているため、MyViewModel
が破棄されると同時に破棄されます。
2.7 ViewModelFactoryの自動生成
Hiltは、@HiltViewModel
アノテーションが付与されたViewModelに対して、ViewModelFactoryを自動的に生成します。これにより、ViewModelをUI Controllerで簡単にインスタンス化できます。
“`kotlin
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels() // Hiltによって注入されたViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
}
}
“`
viewModels()
拡張関数を使用することで、Hiltによって注入されたViewModelを簡単に取得できます。
2.8 HiltによるViewModelのテスト
Hiltはテスト用のモジュールを提供し、テスト環境での依存関係の置き換えを容易にします。
“`kotlin
@UninstallModules(AppModule::class) // テスト対象のモジュールをアンインストール
@HiltAndroidTest
class MyViewModelTest {
@BindValue @JvmField
val myRepository: MyRepository = mockk() // モックの依存関係を提供
@Inject
lateinit var viewModel: MyViewModel
@Before
fun setup() {
HiltAndroidRule(this).inject()
}
@Test
fun myTest() {
// ...
}
}
“`
@UninstallModules
アノテーションは、テスト対象のモジュールをアンインストールするために使用します。@BindValue
アノテーションは、モックの依存関係を提供するために使用します。@HiltAndroidTest
アノテーションは、Hiltテストを実行するために使用します。HiltAndroidRule
は、Hiltテストのセットアップとティアダウンを行います。
2.9 Hiltの利点と欠点
利点:
- 標準化されたアノテーション: DIの設定を簡潔に記述できます。
- 自動生成されるコード: ボイラープレートコードを削減できます。
- Androidフレームワークとの統合: Androidフレームワークのコンポーネントへの依存性注入を容易に行えます。
- テストサポート: テスト環境での依存関係の置き換えを容易にします。
- ViewModelサポート:
@HiltViewModel
アノテーションを使用することで、ViewModelへの依存性注入を簡単に行えます。
欠点:
- 学習コストが高い: Daggerをベースにしているため、DIの概念を理解する必要があります。
- コンパイル時間が長い: コードを自動生成するため、コンパイル時間が長くなることがあります。
- 柔軟性が低い: 標準化されたアノテーションを使用するため、DIの設定の自由度が低いことがあります。
3. KoinによるViewModelへの依存性注入
3.1 Koinの概要と特徴
Koinは、Kotlinで記述された軽量なDIライブラリです。コンパイル時のコード生成を行わず、実行時に依存関係を解決するため、ビルド時間が短く、リフレクションを使用しないためパフォーマンスが良いという特徴があります。
Koinの主な特徴は以下の通りです。
- DSLによる設定: KotlinのDSL(Domain Specific Language)を用いて、DIの設定を記述します。
- 実行時の依存性解決: コンパイル時にコードを生成せず、実行時に依存関係を解決します。
- 軽量で高速: リフレクションを使用しないため、パフォーマンスが良いです。
- Androidフレームワークとの統合: Activity、Fragment、ViewModelなどのAndroidフレームワークのコンポーネントへの依存性注入を容易に行えます。
- テストサポート: テスト環境での依存関係の置き換えを容易にします。
- ViewModelサポート:
viewModel
関数を使用することで、ViewModelへの依存性注入を簡単に行えます。
3.2 Koinのセットアップ:build.gradleの設定
Koinを使用するためには、まずプロジェクトのbuild.gradle
ファイルに以下の依存関係を追加する必要があります。
モジュールレベルのbuild.gradle (app/build.gradle.kts):
kotlin
dependencies {
implementation("io.insert-koin:koin-android:3.5.3")
implementation("io.insert-koin:koin-androidx-compose:3.5.3") // For Compose
testImplementation("io.insert-koin:koin-test:3.5.3") // テスト用
}
上記の例では、Koinのバージョンを3.5.3に設定しています。最新のバージョンは、Koinの公式ドキュメントで確認してください。
また、Koinを使用するためには、アプリケーションクラスでKoinを起動する必要があります。
kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger(Level.ERROR) // Log level
androidContext(this@MyApplication)
modules(appModule) // モジュールリスト
}
}
}
3.3 viewModel
関数によるViewModelの定義
KoinでViewModelを定義するためには、viewModel
関数を使用します。
kotlin
val appModule = module {
single<MyRepository> { MyRepositoryImpl() }
factory { MyUseCase(get()) }
viewModel { MyViewModel(get(), get()) }
}
module
関数は、Koinモジュールを定義します。single
関数は、シングルトンの依存関係を定義します。factory
関数は、毎回新しいインスタンスを提供する依存関係を定義します。viewModel
関数は、ViewModelの依存関係を定義します。get()
関数は、依存関係を解決します。
上記の例では、MyViewModel
はMyRepository
とMyUseCase
の依存関係を持っており、これらの依存関係はget()
関数によって自動的に解決されます。
3.4 get()
関数による依存性の解決
get()
関数は、Koinモジュール内で定義された依存関係を解決するために使用されます。get()
関数は、型推論を使用して依存関係を解決します。
kotlin
viewModel { MyViewModel(get(), get()) } // get() は MyRepository と MyUseCase を解決する
3.5 stateViewModel
関数:SavedStateHandleの利用
stateViewModel
関数を使用すると、ViewModelにSavedStateHandle
を注入できます。SavedStateHandle
は、ViewModelの状態を保存および復元するために使用されます。
“`kotlin
val appModule = module {
viewModel { (handle: SavedStateHandle) -> MyViewModel(get(), get(), handle) }
}
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository,
private val myUseCase: MyUseCase,
private val savedStateHandle: SavedStateHandle // SavedStateHandle
) : ViewModel() {
// …
}
“`
3.6 KoinによるViewModelのテスト
Koinはテスト用のモジュールを提供し、テスト環境での依存関係の置き換えを容易にします。
“`kotlin
class MyViewModelTest : KoinTest {
private val myRepository: MyRepository = mockk()
private val myViewModel: MyViewModel by inject()
@Before
fun setup() {
startKoin {
modules(
module {
single<MyRepository> { myRepository }
viewModel { MyViewModel(get(), get()) }
}
)
}
}
@After
fun tearDown() {
stopKoin()
}
@Test
fun myTest() {
// ...
}
}
“`
KoinTest
インターフェースを実装することで、Koinのテスト機能を利用できます。by inject()
デリゲートを使用して、Koinによって注入された依存関係を取得できます。startKoin()
関数を使用して、テスト用のKoinモジュールを起動します。stopKoin()
関数を使用して、Koinモジュールを停止します。
3.7 Koinの利点と欠点
利点:
- DSLによる設定: KotlinのDSLを用いて、DIの設定を簡潔に記述できます。
- 実行時の依存性解決: コンパイル時にコードを生成せず、実行時に依存関係を解決します。
- 軽量で高速: リフレクションを使用しないため、パフォーマンスが良いです。
- Androidフレームワークとの統合: Androidフレームワークのコンポーネントへの依存性注入を容易に行えます。
- テストサポート: テスト環境での依存関係の置き換えを容易にします。
欠点:
- コンパイル時の安全性がない: 依存関係の解決が実行時に行われるため、コンパイル時にエラーを検出できません。
- 実行時エラーのリスク: 依存関係の解決に失敗した場合、実行時エラーが発生する可能性があります。
- 型安全性の低さ:
get()
関数は型推論を使用するため、型安全性が低いことがあります。
4. Hilt vs Koin:どちらを選ぶべきか
HiltとKoinは、それぞれ異なる特徴を持つDIライブラリです。どちらを選択するかは、プロジェクトの要件や開発チームのスキルセットによって異なります。
4.1 設計思想の違い
- Hilt: Daggerをベースにしており、コンパイル時の依存性解決と型安全性を重視しています。
- Koin: 実行時の依存性解決と軽量さを重視しています。
4.2 学習コストと実装の容易さ
- Hilt: Daggerの知識が必要なため、学習コストが高いです。しかし、自動生成されるコードが多いので、一度理解してしまえば実装自体は比較的容易です。
- Koin: DSLによる設定なので、学習コストが低いです。実装も比較的容易です。
4.3 コンパイル時の安全性と実行時の柔軟性
- Hilt: コンパイル時に依存関係を解決するため、型安全性が高く、コンパイル時にエラーを検出できます。
- Koin: 実行時に依存関係を解決するため、柔軟性が高いです。しかし、コンパイル時にエラーを検出できません。
4.4 パフォーマンスへの影響
- Hilt: コードを自動生成するため、コンパイル時間が長くなることがあります。しかし、実行時のパフォーマンスは高いです。
- Koin: コンパイル時のコード生成を行わないため、コンパイル時間が短いです。しかし、実行時に依存関係を解決するため、Hiltよりもパフォーマンスが低いことがあります。
4.5 チームの規模とプロジェクトの複雑さ
- Hilt: 大規模なプロジェクトや、高い型安全性が求められるプロジェクトに適しています。
- Koin: 小規模なプロジェクトや、迅速な開発が求められるプロジェクトに適しています。
5. 実践的な例:HiltとKoinを用いたViewModelの構築
5.1 シンプルなカウンターアプリの実装(Hilt/Koinそれぞれ)
ここでは、シンプルなカウンターアプリを例に、HiltとKoinを用いたViewModelの実装方法を比較します。
Hilt:
“`kotlin
// Repository
interface CounterRepository {
fun getCount(): Int
fun incrementCount(): Int
}
class CounterRepositoryImpl @Inject constructor() : CounterRepository {
private var count = 0
override fun getCount(): Int {
return count
}
override fun incrementCount(): Int {
count++
return count
}
}
// ViewModel
@HiltViewModel
class CounterViewModel @Inject constructor(
private val counterRepository: CounterRepository
) : ViewModel() {
private val _count = MutableLiveData(counterRepository.getCount())
val count: LiveData<Int> = _count
fun incrementCount() {
_count.value = counterRepository.incrementCount()
}
}
// Module
@Module
@InstallIn(ViewModelComponent::class)
object CounterModule {
@Provides
@ViewModelScoped
fun provideCounterRepository(impl: CounterRepositoryImpl): CounterRepository {
return impl
}
}
// Activity
@AndroidEntryPoint
class CounterActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
viewModel.count.observe(this) { count ->
// Update UI
}
}
fun onIncrementButtonClicked(view: View) {
viewModel.incrementCount()
}
}
“`
Koin:
“`kotlin
// Repository
interface CounterRepository {
fun getCount(): Int
fun incrementCount(): Int
}
class CounterRepositoryImpl : CounterRepository {
private var count = 0
override fun getCount(): Int {
return count
}
override fun incrementCount(): Int {
count++
return count
}
}
// ViewModel
class CounterViewModel(
private val counterRepository: CounterRepository
) : ViewModel() {
private val _count = MutableLiveData(counterRepository.getCount())
val count: LiveData<Int> = _count
fun incrementCount() {
_count.value = counterRepository.incrementCount()
}
}
// Module
val counterModule = module {
single
viewModel { CounterViewModel(get()) }
}
// Activity
class CounterActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
viewModel.count.observe(this) { count ->
// Update UI
}
}
fun onIncrementButtonClicked(view: View) {
viewModel.incrementCount()
}
}
// Application (onCreate)
startKoin {
androidContext(this@MyApplication)
modules(counterModule)
}
“`
5.2 ネットワークリクエストを行うViewModelの実装(Hilt/Koinそれぞれ)
ここでは、Retrofitを用いてネットワークリクエストを行うViewModelの実装方法を比較します。
Hilt:
“`kotlin
// Api Service
interface ApiService {
@GET(“todos/1”)
suspend fun getTodo(): Todo
}
// Retrofit Instance (Module)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(“https://jsonplaceholder.typicode.com/”)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Repository
class TodoRepository @Inject constructor(private val apiService: ApiService) {
suspend fun getTodo(): Todo {
return apiService.getTodo()
}
}
// ViewModel
@HiltViewModel
class TodoViewModel @Inject constructor(private val todoRepository: TodoRepository) : ViewModel() {
private val _todo = MutableLiveData
val todo: LiveData
init {
viewModelScope.launch {
_todo.value = todoRepository.getTodo()
}
}
}
// Module
@Module
@InstallIn(ViewModelComponent::class)
object TodoModule {
@Provides
@ViewModelScoped
fun provideTodoRepository(impl: TodoRepository): TodoRepository {
return impl
}
}
“`
Koin:
“`kotlin
// Api Service
interface ApiService {
@GET(“todos/1”)
suspend fun getTodo(): Todo
}
// Retrofit Instance (Module)
val networkModule = module {
single {
Retrofit.Builder()
.baseUrl(“https://jsonplaceholder.typicode.com/”)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single { get
}
// Repository
class TodoRepository(private val apiService: ApiService) {
suspend fun getTodo(): Todo {
return apiService.getTodo()
}
}
// ViewModel
class TodoViewModel(private val todoRepository: TodoRepository) : ViewModel() {
private val _todo = MutableLiveData
val todo: LiveData
init {
viewModelScope.launch {
_todo.value = todoRepository.getTodo()
}
}
}
// Module
val todoModule = module {
single { TodoRepository(get()) }
viewModel { TodoViewModel(get()) }
}
// Application (onCreate)
startKoin {
androidContext(this@MyApplication)
modules(networkModule, todoModule)
}
“`
5.3 データベースアクセスを行うViewModelの実装(Hilt/Koinそれぞれ)
ここでは、Roomを用いてデータベースアクセスを行うViewModelの実装方法を比較します。
(この部分には、Roomに関する定義とコードが含まれます。サンプルコードの長さ制限のため、詳細な実装は省略します。基本的な構造は上記の例と同様で、RepositoryがRoomデータベースへのアクセスを担当し、ViewModelがRepositoryを介してデータを取得および更新します。 HiltとKoinの違いは、モジュール定義とViewModelのインスタンス化方法にあります。)
6. ViewModelとStateFlow/LiveDataの連携
ViewModelは、UIにデータを通知するために、LiveDataやStateFlowなどのオブザーバブルなデータホルダーを使用します。
6.1 StateFlowとLiveDataの選択
- LiveData: Android Architecture Componentsの一部であり、ライフサイクルを意識したオブザーバブルなデータホルダーです。Javaとの互換性が高く、従来のAndroid開発で広く使用されています。
- StateFlow: Kotlin Coroutinesの一部であり、コールドなフローとして動作します。ホットなフローである
SharedFlow
も存在します。Kotlinを多用するプロジェクトや、より高度な非同期処理が必要な場合に適しています。
6.2 Hilt/KoinとStateFlow/LiveDataの組み合わせ
HiltとKoinは、LiveDataやStateFlowと組み合わせて使用できます。
“`kotlin
// Hilt
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository
) : ViewModel() {
private val _stateFlow = MutableStateFlow<DataState>(DataState.Loading)
val stateFlow: StateFlow<DataState> = _stateFlow.asStateFlow()
init {
viewModelScope.launch {
try {
val data = myRepository.getData()
_stateFlow.value = DataState.Success(data)
} catch (e: Exception) {
_stateFlow.value = DataState.Error(e.message ?: "Unknown error")
}
}
}
}
// Koin
class MyViewModel(
private val myRepository: MyRepository
) : ViewModel() {
private val _stateFlow = MutableStateFlow<DataState>(DataState.Loading)
val stateFlow: StateFlow<DataState> = _stateFlow.asStateFlow()
init {
viewModelScope.launch {
try {
val data = myRepository.getData()
_stateFlow.value = DataState.Success(data)
} catch (e: Exception) {
_stateFlow.value = DataState.Error(e.message ?: "Unknown error")
}
}
}
}
“`
6.3 UIへのデータの反映
UIでは、LiveDataやStateFlowを監視し、データが変更された際にUIを更新します。
“`kotlin
// Activity (Compose)
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val state = viewModel.stateFlow.collectAsState()
when (state.value) {
is DataState.Loading -> {
Text("Loading...")
}
is DataState.Success -> {
Text("Data: ${(state.value as DataState.Success).data}")
}
is DataState.Error -> {
Text("Error: ${(state.value as DataState.Error).message}")
}
}
}
“`
7. まとめ:最適な依存性注入戦略の選択
本記事では、KotlinでViewModelと依存性注入ライブラリHiltとKoinを連携させる方法を詳細に解説しました。
7.1 プロジェクトの要件に合わせたライブラリの選択
HiltとKoinは、それぞれ異なる特徴を持つDIライブラリです。どちらを選択するかは、プロジェクトの要件や開発チームのスキルセットによって異なります。
- Hilt: 大規模なプロジェクトや、高い型安全性が求められるプロジェクトに適しています。
- Koin: 小規模なプロジェクトや、迅速な開発が求められるプロジェクトに適しています。
7.2 依存性注入のベストプラクティス
- コンストラクタインジェクションを優先する: コンストラクタインジェクションは、最も推奨されるDIの方法です。
- インターフェースを活用する: 依存関係を抽象化することで、疎結合なコードを作成できます。
- シングルトンを慎重に利用する: シングルトンの使用は、テストを困難にする可能性があるため、慎重に検討する必要があります。
- モジュールを適切に分割する: モジュールを適切に分割することで、コードの可読性と保守性を向上させることができます。
7.3 今後の学習への道しるべ
本記事で解説した内容は、ViewModelと依存性注入の基礎的な知識です。さらに高度なDIのテクニックや、LiveData/StateFlowのより詳細な使い方を学習することで、より高品質なAndroidアプリを開発できるようになります。
- Dagger/Hiltの詳細: Hiltの基盤となっているDaggerを深く理解することで、より高度なDIの設定が可能になります。
- Kotlin CoroutinesとFlow: StateFlowやSharedFlowなどのKotlin CoroutinesのFlowを深く理解することで、より効率的な非同期処理が可能になります。
- テスト駆動開発(TDD): テストを先に記述することで、よりテストしやすいコードを作成できます。
本記事が、あなたの