koin について
Kotlin Android と KMP の開発者向けの Koin 依存性注入リファレンススキルです。クロスプラットフォームのモジュール構成、Compose、スコープ、依存グラフ検証の注意点を扱います。
主な機能
- 01KMP のソースセット別依存関係構成
- 02KSP によるモジュール生成
- 03Compose の ViewModel 注入
- 04保持スコープとカスタムスコープ
- 05verify() による依存グラフ検査
活用シーン
- 01プラットフォーム別依存関係の設定
- 02アノテーション付きモジュールの生成と登録
- 03構成変更をまたぐオブジェクトの管理
- 04テストで未定義のバインディングを検出
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rcosteira79/ android-skills --skill koinインストール後、Claude Code で/koinを入力して読み込まれたか確認します。
詳細
Koin 依存性注入(Android と KMP)
実用的な Kotlin DI です。Koin と Hilt の比較: Koin は commonMain で動作します(Hilt は動作しません)。コード生成の代わりに verify() でグラフを検証します。Hilt はコード生成と Jetpack との深い統合(@HiltViewModel / hiltViewModel())を備えた Android 専用です。このリファレンスでは、Classic DSL の基本(single / factory / scoped / viewModel { } と get() による解決および実行時パラメーター、koinViewModel / koinInject、startKoin { androidLogger(); androidContext(this@MyApp); modules(...) })ではなく、注意点と境界を扱います。関連: android-skills:kmp-ktor、android-skills:android-data-layer。依存関係は koin-bom 経由で追加します:koin-core、koin-android、koin-androidx-compose、koin-compose-viewmodel(KMP)、koin-compose-viewmodel-navigation(Nav 3)、koin-test、および任意で koin-annotations / koin-ksp-compiler。
KSP アノテーション — 生成されるモジュールアクセサー
@Module @ComponentScan はアノテーション付きクラスを検出し、startKoin に UserModule().module として渡すモジュールを生成します。アノテーション付きクラス上の生成された .module 拡張が、最も見落とされやすい部分です。モジュールごとにスタイルを一つ選んでください(単一のモジュール内で Classic DSL とアノテーションを混在させないでください。モジュール間で切り替えるのは問題ありません)。
@Single class UserRepositoryImpl(private val service: UserService) : UserRepository
@Factory class UserFormValidator
@KoinViewModel class UserDetailViewModel(
@InjectedParam private val userId: String,
private val repository: UserRepository,
) : ViewModel()
@Module @ComponentScan("com.example.feature.user") class UserModule
// startKoin { modules(UserModule().module) }
KMP のソースセット構成
ほとんどのバインディングは commonMain に置きます。HTTP エンジン、Context、Keychain など、プラットフォーム固有の型を使うものは、各プラットフォームの actual を伴う expect val platformModule: Module の背後に置きます(android-skills:kmp-ktor のエンジンパターンと同様です)。iOS の起動時には、iOSApp.init() から InitKoinKt.doInitKoin(config: nil) を呼び出します。Swift では init が予約語のため、do の接頭辞が付いています。
// commonMain
expect val platformModule: Module
// androidMain: actual = module { single<HttpClientEngine> { OkHttp.create() }; single<TokenStorage> { DataStoreTokenStorage(get()) } }
// iosMain: actual = module { single<HttpClientEngine> { Darwin.create() }; single<TokenStorage> { KeychainTokenStorage() } }
Compose:呼び出し箇所での失敗
koinViewModel / koinInject には koin-androidx-compose(Android)または koin-compose-viewmodel(KMP)が必要です。このアーティファクトがない場合、呼び出しは コンパイルできますが、呼び出し箇所で失敗します。koin-core は ViewModel や Compose ランタイムについて何も知りません。テストしやすくするため、Composable のパラメーターには koinInject() をデフォルト値として指定します:fun Screen(service: AnalyticsService = koinInject())。
val vm: UserDetailViewModel = koinViewModel { parametersOf(userId) }
// keyed per entity: koinViewModel(key = "detail_$userId", parameters = { parametersOf(userId) })
スコープ
構成変更後も存続させる(一般的なケース)→ activityRetainedScope — Activity の保持された ViewModel によって維持されるため、画面の回転をまたいで存続し、Activity の実際の終了時に閉じられます。activityRetainedScope { }(または @ActivityRetainedScope)で宣言し、AndroidScopeComponent 経由でアクセスします。このケースで createScope / close を独自に実装しないでください。
val checkoutModule = module {
activityRetainedScope { // retained across config changes
scoped { CheckoutCart() }
scoped { CheckoutPricing(get()) }
}
}
class CheckoutActivity : AppCompatActivity(), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()
private val cart: CheckoutCart by inject()
}
Activity に紐付かないカスタムの存続期間(例:注文 ID をキーとする複数ステップのフロー)→ scope<T> を宣言し、そのライフサイクルを管理します:getKoin().createScope<CheckoutFlow>("checkout-$orderId") で作成し、scope.get() で解決し、フローの終了時に scope.close() を呼び出します。
Nav 3
koinEntryProvider() は各デスティネーションごとに ViewModel を解決します。デスティネーションは NavDisplay の呼び出し箇所ではなく、モジュール内に登録してください。
val navigationModule = module {
navigation<HomeRoute> { HomeScreen(viewModel = koinViewModel()) }
navigation<DetailRoute> { route -> DetailScreen(viewModel = koinViewModel { parametersOf(route.id) }) }
}
NavDisplay(backStack = backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = koinEntryProvider())
テスト — verify() / checkModules()
各宣言のコンストラクターをたどり、すべての依存関係が宣言されていることを確認します。不足しているバインディングは、実行時の NoDefinitionFoundException ではなく テストの失敗になります(Hilt の利点の一つを補います)。CI で実行してください。実行時に解決するコンストラクターパラメーター(例:SavedStateHandle)については、extraTypes を渡してください。渡さないと verify() が誤検出します。実際のバインディングはフェイクで置き換えます(例:Ktor の MockEngine。android-skills:kmp-ktor を参照)。KoinTestRule(JUnit 4)/ KoinTestExtension(JUnit 5)は、テストごとにコンテキストをセットアップします。
class ModuleVerificationTest : KoinTest {
@Test fun `all modules resolve cleanly`() {
koinApplication { modules(appModule, networkModule, platformModule) }.checkModules()
}
// Per-module with runtime-resolved types:
// featureModule.verify(extraTypes = listOf(SavedStateHandle::class))
}