Skip to content

Commit 44194e7

Browse files
1.0.0-RC2 bump
1 parent 538004f commit 44194e7

4 files changed

Lines changed: 105 additions & 2 deletions

File tree

docs/COMPILE_TIME_SAFETY.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,83 @@ class Other(@Property("missing.key") val value: String)
225225
// WARNING — no @PropertyValue("missing.key") found
226226
```
227227

228+
## Module Load Order and Overrides
229+
230+
Koin is **last-wins** at runtime: when two modules define the same type, the one loaded last takes precedence. The compiler plugin assembles the module list at the `@KoinApplication` root in this order:
231+
232+
1. **Auto-discovered `@Configuration` modules** (this compilation + dependency JARs) — load first
233+
2. **Explicit `@KoinApplication(modules = [A, B, C])`** — load last, **in declaration order**
234+
235+
The rationale: apps customise libraries, not the other way round. So the app's explicit list wins over dependency-provided defaults.
236+
237+
```kotlin
238+
// Dependency JAR — default implementation
239+
@Module @Configuration
240+
class CoreModule {
241+
@Singleton fun feature(): Feature = DefaultFeature()
242+
}
243+
244+
// App — custom override
245+
@Module
246+
class AppModule {
247+
@Singleton fun feature(): Feature = AppFeature()
248+
}
249+
250+
@KoinApplication(modules = [AppModule::class])
251+
class MyApp
252+
// Load order: CoreModule (DefaultFeature) → AppModule (AppFeature wins)
253+
```
254+
255+
Within the explicit list, declaration order is preserved:
256+
257+
```kotlin
258+
@KoinApplication(modules = [A::class, B::class, C::class])
259+
// Load order: (@Configuration deps) → A → A.includes → B → B.includes → C → C.includes
260+
// Winner among A/B/C: C (declared last)
261+
```
262+
263+
If a module re-appears in both the explicit list and is also discovered via `@Configuration`, it is loaded once — at its **explicit position** — so the user's declaration order always controls override precedence.
264+
265+
**Escape hatch for fine-grained ordering**: list all participating modules explicitly in `@KoinApplication(modules = [...])` in the desired order. This bypasses classpath-dependent discovery order for `@Configuration` modules.
266+
267+
## Generic DSL Types
268+
269+
Runtime Koin resolves definitions on the **erased raw class** — type parameters are not part of the lookup key. Compile-safety honours that: a `get<Box<X>>()` call is validated against any `Box<*>` provider in the graph, and two `single<Box<A>>()` / `single<Box<B>>()` declarations collide on the same raw class.
270+
271+
```kotlin
272+
val appModule = module {
273+
single<Navigator<AppKey>>() // validated as Navigator (raw)
274+
single<Navigator<MenuKey>>() // treated as the same definition
275+
}
276+
```
277+
278+
Validating on the raw class is also what makes iOS/Native builds work — emitting the generic type with its free parameter into hint functions used to crash the Kotlin/Native klib signature mangler.
279+
280+
### Discriminating generic instances — use `named<T>()`
281+
282+
When multiple instances of the same generic class must coexist, register a **concrete wrapper type** and key each instance with a type qualifier derived from the generic parameter. This is the pattern used internally by `koin-compose-navigation3`:
283+
284+
```kotlin
285+
// From koin-compose-navigation3
286+
inline fun <reified T : Any> Module.navigation(
287+
noinline definition: @Composable Scope.(T) -> Unit,
288+
): KoinDefinition<EntryProviderInstaller> {
289+
// Concrete type EntryProviderInstaller + type qualifier derived from T.
290+
return _singleInstanceFactory<EntryProviderInstaller>(named<T>(), { ... })
291+
}
292+
293+
// Caller side
294+
module {
295+
navigation<HomeRoute> { ... } // keyed by named<HomeRoute>()
296+
navigation<SettingsRoute> { ... } // keyed by named<SettingsRoute>()
297+
}
298+
299+
// Resolution uses the same type qualifier
300+
koin.get<EntryProviderInstaller>(named<HomeRoute>())
301+
```
302+
303+
Runtime Koin matches on (raw class + qualifier), and the compile-safety validator respects qualifier matching — so the plugin sees these as two distinct definitions, as expected. Prefer `named<T>()` qualifier-on-concrete-type over `single<Box<X>>()` directly whenever you need to distinguish generic instantiations.
304+
228305
## Configuration
229306

230307
```kotlin

docs/DEBUGGING.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,32 @@ val instanceExpression = if (includedModuleClass.isObject) {
969969

970970
**Prevention**: When actively developing the plugin, consider keeping `test-apps` in a separate IntelliJ window and refreshing Gradle after each `./install.sh`.
971971

972+
### 7.13 "'buildViewModel' is not on classpath. Add dependency: io.insert-koin:koin-core-viewmodel"
973+
974+
**Symptom**: Compile error at an `@KoinViewModel`-annotated class:
975+
```
976+
[Koin] @KoinViewModel definition 'com.app.MyViewModel' cannot be generated:
977+
'buildViewModel' is not on classpath. Add dependency: io.insert-koin:koin-core-viewmodel
978+
```
979+
980+
Same shape for `@KoinWorker``io.insert-koin:koin-android-workmanager`.
981+
982+
**Cause**: `@KoinViewModel` / `@KoinWorker` annotations live in `koin-annotations` (always available via `koin-core`), but the runtime DSL that backs them (`Module.buildViewModel` / `Module.buildWorker`) ships in a separate artifact. If you only have `koin-core` + `koin-annotations`, the plugin can't generate a working definition for those annotations.
983+
984+
Before the check existed (pre-RC2.3), the plugin silently skipped the definition; you got `NoDefinitionFoundException` at runtime. RC2.3+ fails loudly with the fix instruction.
985+
986+
**Fix**:
987+
988+
```kotlin
989+
// build.gradle.kts
990+
dependencies {
991+
implementation("io.insert-koin:koin-core-viewmodel:4.2.1") // for @KoinViewModel
992+
implementation("io.insert-koin:koin-android-workmanager:4.2.1") // for @KoinWorker (Android)
993+
}
994+
```
995+
996+
The error fires once per annotation type per compilation — if you see it for `@KoinViewModel`, all ViewModel definitions in that compilation are blocked until the artifact is added.
997+
972998
---
973999

9741000
## 8. Development Workflow

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
kotlin.code.style=official
22

33
# Plugin version
4-
pluginVersion=1.0.0-RC2.4
4+
pluginVersion=1.0.0-RC2
55

66
# Gradle
77
org.gradle.daemon=true

test-apps/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[versions]
22
kotlin = "2.3.20"
33
koin = "4.2.1"
4-
koin-plugin = "1.0.0-RC2.4"
4+
koin-plugin = "1.0.0-RC2"
55

66
[libraries]
77
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }

0 commit comments

Comments
 (0)