Quick Start — first worker in 60 seconds¶
This walk-through gets a background sync worker running on all four platforms (Android, iOS, Desktop, Web) with the same commonMain code. We assume you already have a Kotlin Multiplatform project skeleton — if not, see Installation first.
Goal: by the end of this page you'll have a
DataSyncWorkerdefined incommonMain, wired through Koin, scheduled viaWorkManager.enqueue(...), and emitting state changes you can observe from any platform target.
Step 1 — Define a worker in commonMain¶
The same worker class will execute on every platform you target. No expect / actual needed for the worker logic itself.
// commonMain/kotlin/com/example/work/DataSyncWorker.kt
package com.example.work
import io.github.mobilebytelabs.worker.CoroutineWorker
import io.github.mobilebytelabs.worker.WorkResult
import io.github.mobilebytelabs.worker.WorkerContext
class DataSyncWorker(
context: WorkerContext,
private val api: ApiClient,
) : CoroutineWorker(context) {
override suspend fun doWork(): WorkResult {
val endpoint = inputData.getString("endpoint")
?: return WorkResult.failure("missing endpoint")
return runCatching { api.sync(endpoint) }.fold(
onSuccess = { WorkResult.success(workDataOf("synced" to it.count)) },
onFailure = { WorkResult.retry(it.message) },
)
}
}
Key points:
- CoroutineWorker is suspendable — call any suspending API from doWork().
- Return WorkResult.success / failure / retry — semantics are identical on every platform.
- inputData is a typed key-value bag — see WorkData docs in Home.
Step 2 — Declare your workers + wire Koin in commonMain¶
worker-kmp v4 is a single-API commonMain setup. You (1) declare your workers with one
annotation, and (2) call the codegen-emitted WorkerKmpAuto.install() from your shared init.
The per-platform factory selection, worker registry, and Koin wiring are all generated for you —
you write zero per-platform worker code. (Applying the io.github.mobilebytelabs.worker-app
Gradle plugin — see Installation / Convention Plugin —
is what runs the codegen.)
// commonMain — declare the workers once; the annotation drives codegen for every platform.
import io.github.mobilebytelabs.worker.app.WorkerKmpWorkers
@WorkerKmpWorkers(workers = [DataSyncWorker::class])
fun workerDeclarations() = Unit
// commonMain — your app's shared init (the ONE function every platform entry point calls).
import cmp.shared.generated.WorkerKmpAuto // codegen'd from @WorkerKmpWorkers into your module
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module
fun initApp(config: KoinAppDeclaration? = null) {
startKoin {
config?.invoke(this) // Android binds androidContext(this@App) here
modules(module { single { ApiClient() } })
}
WorkerKmpAuto.install() // ONE line — wires workers on Android/iOS/Desktop/Web
}
Each platform entry point just calls initApp() — no per-platform worker code:
// androidMain — the app class only supplies the Koin context:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
initApp { androidContext(this@MyApp) }
}
}
// desktopMain: fun main() { initApp(); /* compose window */ }
// wasmJsMain / jsMain: fun main() { initApp(); /* compose viewport */ }
// iosMain: ViewController { initApp(); /* compose UIViewController */ }
Placement matters: put
WorkerKmpAuto.install()in the shared commonMain init (as above), not in a single platform's app class — otherwise the other platforms compile and run but schedule no workers. See the Single-API Guide for the full multiplatform-placement rationale.
Foreground-service registration, BGTaskScheduler task IDs, daemon installation, and Service
Worker registration are all handled by the library — you don't write any of it.
Step 3 — Schedule work¶
From any commonMain composable, ViewModel, or coroutine:
class SyncViewModel(private val workManager: WorkManager) {
fun startSync() {
workManager.enqueue(oneTimeWorkRequest<DataSyncWorker> {
setInputData(workDataOf("endpoint" to "/api/sync"))
setConstraints(Constraints {
setRequiredNetworkType(NetworkType.CONNECTED)
setRequiresBatteryNotLow(true)
})
setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30.seconds)
addTag("sync")
})
}
}
The Constraints { … } DSL produces a builder whose semantics are identical on every platform. On Android they map to WorkManager.Constraints; on iOS to BGTaskScheduler predicates; on Desktop to in-process reachability + battery checks; on Web to navigator.connection + Service Worker sync events.
Step 4 — Observe state changes¶
The Flow<List<WorkInfo>> API works the same on every platform:
class SyncScreen(workManager: WorkManager) {
val syncStatus: Flow<WorkInfoState> =
workManager.getWorkInfosByTag("sync")
.map { infos -> infos.firstOrNull()?.state ?: WorkInfoState.UNKNOWN }
}
Or use the Compose Multiplatform UI components shipped in cmp-worker-compose:
@Composable
fun SyncDashboard() {
Column {
WorkSchedulerScreen(onWorkScheduled = { id -> /* navigate to detail */ })
WorkMonitorScreen(tag = "sync")
}
}
WorkMonitorScreen shows real-time state transitions, retry attempts, and progress events for every worker matching the tag. Drop it anywhere — it's the same composable on Android, iOS, Desktop, and Web.
What's next¶
- Platform-specific setup (Manifest / Info.plist / installer entries): Android · iOS · Desktop · Web
- Long-running tasks that must keep the OS awake: Foreground Tasks
- Telemetry — observe every worker lifecycle event from your OTel / Sentry / Firebase Perf SDK: Observers
- Web Push server setup for true-background Web workers: Web Push Server