Backgrounder¶
One Kotlin Multiplatform API for background work. Schedule a job from commonMain; it runs on WorkManager on Android, on BGTaskScheduler on iOS, and on NSBackgroundActivityScheduler on native macOS. No DI container required — workers are factory-built per dispatch from a closure you provide, so any DI graph you already have (Koin, Hilt, hand-wired) plugs in cleanly.
// commonMain
class SyncWorker(private val repo: MyRepository) : BackgroundWorker {
override suspend fun execute(context: WorkerContext): WorkResult {
repo.sync()
return WorkResult.Success
}
companion object {
val ID = TaskId("dev.example.app.sync")
}
}
// At app launch
val backgrounder = Backgrounder.create(application = this) // Android
backgrounder.register(SyncWorker.ID) { SyncWorker(repo = appGraph.repo) }
backgrounder.start()
// Anywhere later
backgrounder.scheduler.schedule(
WorkRequest.OneTime(
taskId = SyncWorker.ID,
constraints = WorkConstraints(networkRequired = NetworkRequirement.Any),
backoff = BackoffPolicy.exponential(initialDelay = 30.seconds, maxAttempts = 5),
),
)
What it does¶
- One scheduling API across platforms —
Scheduler.schedule(),cancel(),cancelAll(),scheduled(). - One worker contract —
BackgroundWorker.execute(WorkerContext): WorkResult. Inject your dependencies through the factory closure you register at app launch. - Sealed
WorkRequest—OneTimeandPeriodic, both with input data, constraints, retry, and anephemeralflag for the "ran-before-init" Android foot-gun. - Instant dispatch —
Backgrounder.runNow<R>(taskId) { … }runs a lambda in the background right now and suspends until the typed result is back. Bypasses constraints / retries / the registry; routed through the platform's real background primitive so the work survives an immediate app-background. - Honest about platform differences.
Scheduler.guarantees()returns a per-platform truth table you can branch UX on. - No required DI dependency. Backgrounder doesn't ship a DI module. The factory closure pattern works equally well with Koin, Hilt (Android), kotlin-inject, or hand-wired graphs.
Why this exists¶
Android has WorkManager — rich, persistent, constraint-aware. Apple platforms have BGTaskScheduler and NSBackgroundActivityScheduler — different shape, different guarantees, opaque scheduling. The two worlds don't line up cleanly, and most KMP projects either:
- Roll their own thin wrapper that pretends they do (and quietly drops on the floor whatever doesn't translate), or
- Implement background work twice, once per platform, sharing nothing.
Backgrounder takes a third path: a shared API that's honest about what each platform actually guarantees, so consumers can write platform-aware UX (e.g. "Open the app daily so we can sync" on iOS only, where force-quit kills background tasks) without re-implementing scheduling itself.
Get started¶
- Getting started — zero-to-working in ~10 minutes.
- Installation — Gradle DSL + version-catalog snippets, OS-floor table.
- Concepts → Architecture — the three-layer design.
- Platforms → Force-quit caveat (iOS) — the single most-often-misunderstood thing about iOS background work. Read this before shipping.
iOS limitation
When the user force-quits the app from the App Switcher, all background tasks stop firing until the user launches the app again. That's Apple's design; we can't paper over it. See Force-quit on iOS for what to surface in your UX.