Skip to content

single api guide

github-actions[bot] edited this page Jun 4, 2026 · 4 revisions

worker-kmp Single-API Guide (v4.0.0+)

End-to-end consumer-facing guide for the worker-kmp single-API in commonMain. After this epic ships (worker-kmp-single-api-completion), consumers write 100% commonMain code for the worker-kmp scheduling/observation/init/Koin domain. Per-platform glue is auto-generated by cmp-worker-app-plugin at build time.

Quick start

// commonMain — entire worker-kmp setup
@WorkerKmpApp(
    title = "My App",
    iosBundleId = "com.example.myapp",
)
public fun appKoinModules(): List<Module> = listOf(
    DataModule,
    SyncObserverKoinModule,  // from cmp-worker-sync — binds UniqueWorkObserver
)

@WorkerKmpWorkers(workers = [DataSyncWorker::class, NotificationWorker::class])
public fun workerDeclarations() = Unit

public class DataSyncWorker(
    context: WorkerContext,
    private val repo: CurrencyRepository,   // public
) : CoroutineWorker(context) { /* ... */ }
// androidMain (or your app's Application.onCreate)
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
            modules(appKoinModules())
        }
        WorkerKmpAuto.install()   // single line — codegen handles the rest
    }
}

That's it. iosMain / desktopMain / wasmJsMain source sets have ZERO consumer-written files for the worker-kmp domain.

Two integration shapes

Shape 1 — full app codegen (no existing Application class)

Use both @WorkerKmpApp + @WorkerKmpWorkers. The plugin generates per-platform launchers (Android Application + Activity, iOS MainViewController, Desktop main(), Web main()) PLUS the worker-init files. Consumer's commonMain code is sufficient — no per-platform shell classes needed.

Shape 2 — bring-your-own-Application (existing app shell)

Use only @WorkerKmpWorkers. The plugin skips launcher generation; consumer writes their own Application.onCreate and calls WorkerKmpAuto.install() after startKoin { ... }. The samples/kmp-project-template/cmp-android/AndroidApp.kt uses this shape (existing Coil setup, language restoration, etc.).

Annotation reference

@WorkerKmpWorkers

  • Goes on a top-level commonMain function (typically public fun workerDeclarations() = Unit)
  • workers: Array<KClass<*>> — each must extend CoroutineWorker with WorkerContext as first primary-constructor param + be public
  • OPTIONAL — omit if no workers needed (e.g. CMP app shell only)
  • Within-module aggregation — multiple sites in the SAME module are aggregated; cross- module aggregation is NOT supported (place annotation in the module that depends on every worker-owning module)

@WorkerForPlatforms([Platform.Web, Platform.Android, ...])

  • Co-annotation on worker classes — opts the worker into a subset of generated init files
  • Default (annotation absent): worker registered on all 4 platforms

@WorkerKmpApp(title, iosBundleId, webCanvasId?, androidApplicationId?, androidPermissions?)

  • Goes on public fun appKoinModules(): List<Module> — REMOVED the v3.1.x factory: WorkManagerFactory parameter (codegen handles factory selection now)

Visibility constraint

All worker classes AND their primary-constructor dep types MUST be public. KSP processor errors with clear suggested-fix message if you use internal class or private class — codegen-emitted code lives in your cmp-shared/build/generated/... and can only reference public symbols across module boundaries.

Default-valued constructor params

public class IntervalWorker(
    ctx: WorkerContext,
    val intervalMs: Long = 5000L,  // SKIPPED from Koin autowiring — default used at runtime
) : CoroutineWorker(ctx) { ... }

KSP detects KSValueParameter.hasDefault == true and emits register<IntervalWorker> { ctx -> IntervalWorker(context = ctx) } (no getKoin().get<Long>()).

Generic Koin dep types — require @Named qualifier

// ❌ Compile error — Koin runtime erases generic type args
public class GenericWorker(
    ctx: WorkerContext,
    val store: Store<String, ExchangeRates>,
) : CoroutineWorker(ctx)

// ✅ Use @Named qualifier
public class GenericWorker(
    ctx: WorkerContext,
    @Named("exchange-rates") val store: Store<String, ExchangeRates>,
) : CoroutineWorker(ctx)

// + matching Koin binding
single<Store<String, ExchangeRates>>(named("exchange-rates")) { ... }

Calling order discipline (Android)

startKoin { androidContext(this) } MUST run BEFORE WorkerKmpAuto.install() — the Android actual reads Context from the androidContext() binding. If called in wrong order, the shim throws IllegalStateException with a clear message pointing at the fix.

WorkerKmpHost.initialize is NON-SUSPEND

Pure setup state — safe to call from Application.onCreate without runBlocking (eliminated ANR risk vs v3.1.x's hypothetical suspend approach). No first-sync is enqueued — that's consumer's responsibility after WorkerKmpAuto.install() returns (e.g. get<WorkManager>().enqueueUniqueWork(...) from your own bootstrap code).

Configuration

// Customize via Koin binding (D22 — annotations can't carry data-class instances)
val MyAppModule = module {
    single { WorkerKmpHostConfig(logTag = "my-app.worker") }
}

Defaults: koinScopeQualifier = null (global scope), logTag = "worker-kmp.host".

Test path

Koin module overrides — allowOverride = true is REQUIRED:

@Before
fun setUp() {
    startKoin { modules(appKoinModules()) }
    WorkerKmpAuto.install()

    // Replace the codegen-bound WorkManager with a fake
    loadKoinModules(
        module {
            single<WorkManager>(allowOverride = true) { FakeWorkManager() }
        },
    )
}

For pure-commonMain unit tests of workers (no factory needed), instantiate the worker class directly with a fake WorkerContext (same pattern existing worker-kmp samples already use).

Migration from v3.1.x

v3.1.x v4.0.0
appKoinModules(factory: WorkManagerFactory): List<Module> appKoinModules(): List<Module>
Manual workerRegistry { register<W> { … } } block @WorkerKmpWorkers(workers = [W::class])
loadKoinModules(workKoinModule(WorkerConfig(), registry, factory)) WorkerKmpAuto.install()
Ad-hoc Sync.initialize(scheduler) host entry-point WorkerKmpHost.initialize() (called by codegen)
Per-platform SyncManager actuals cmp-worker-sync's UniqueWorkObserver (commonMain-only)

The old workKoinModule(config, workers, factory) function exists as a @Deprecated(level = ERROR) tombstone with ReplaceWith("WorkerKmpAuto.install()") for IDE quick-fix migration.

Source-set discipline GUARANTEE

After migration, your consumer's per-platform source sets contain ZERO files for the worker-kmp scheduling/observation/init/Koin domain. Verify:

find your-app/src/{androidMain,iosMain,desktopMain,wasmJsMain} \
    \( -name 'SyncManager*.kt' \
     -o -name 'WorkScheduler*.kt' \
     -o -name 'WorkerKmpAuto.kt' \
     -o -name 'Sync*Initializer.kt' \) \
    -not -path '*/build/*'

Expected output: EMPTY.

Related

Clone this wiki locally