Skip to content

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 DataSyncWorker defined in commonMain, wired through Koin, scheduled via WorkManager.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) },
        )
    }
}

Screenshot placeholder: docs/_images/quick-start/step1-worker.png

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 — Wire up Koin in commonMain

worker-kmp ships a single Koin DI module. Call it once at app start with your platform factory. Everything else — work scheduling, persistence, OS APIs — is handled internally by the library.

// commonMain/kotlin/com/example/di/AppModule.kt
import io.github.mobilebytelabs.worker.koin.workKoinModule
import io.github.mobilebytelabs.worker.koin.workerRegistry
import io.github.mobilebytelabs.worker.WorkerConfig

fun appKoinModule(platformFactory: WorkManagerFactory) = module {
    single { ApiClient() }
}

fun startWorkerKoin(platformFactory: WorkManagerFactory) {
    startKoin {
        modules(
            workKoinModule(
                config = WorkerConfig(logLevel = LogLevel.INFO),
                workers = workerRegistry {
                    register<DataSyncWorker> { ctx -> DataSyncWorker(ctx, get()) }
                },
                factory = platformFactory,
            ),
            appKoinModule(platformFactory),
        )
    }
}

Screenshot placeholder: docs/_images/quick-start/step2-koin.png

Each platform's entry-point passes the platform factory:

// Android (Application.onCreate)
startWorkerKoin(androidWorkManagerFactory(this))

// iOS (commonMain main viewModel or AppDelegate Kotlin bridge)
startWorkerKoin(iosWorkManagerFactory())

// Desktop (main.kt)
startWorkerKoin(desktopWorkManagerFactory())

// Web (commonMain main + JS / Wasm entry)
startWorkerKoin(webWorkManagerFactory())

The platform factory is the only per-platform code you write. Everything else — including foreground-service registration, BGTaskScheduler task IDs, daemon installation, and Service Worker registration — is handled by the library.


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")
        })
    }
}

Screenshot placeholder: docs/_images/quick-start/step3-enqueue.png

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")
    }
}

Screenshot placeholder: docs/_images/quick-start/step4-monitor.png

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