mcpField
K
koin

koin

github.com/rcosteira79開発165133 回インストール6 週間前に更新
リポジトリ

koin について

Kotlin Android と KMP の開発者向けの Koin 依存性注入リファレンススキルです。クロスプラットフォームのモジュール構成、Compose、スコープ、依存グラフ検証の注意点を扱います。

主な機能

  1. 01KMP のソースセット別依存関係構成
  2. 02KSP によるモジュール生成
  3. 03Compose の ViewModel 注入
  4. 04保持スコープとカスタムスコープ
  5. 05verify() による依存グラフ検査

活用シーン

  1. 01プラットフォーム別依存関係の設定
  2. 02アノテーション付きモジュールの生成と登録
  3. 03構成変更をまたぐオブジェクトの管理
  4. 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))
}

コメント