Architecture¶
Backgrounder is three layers, all in /backgrounder:
┌────────────────────────────────────────────────────────────────────────┐
│ commonMain — public API │
│ BackgroundWorker, WorkerContext, WorkRequest, WorkResult │
│ BackgroundWorkerFactory — bulk task-id registration │
│ WorkerRegistry — task-id → factory map (the DI seam) │
│ EphemeralRegistry — cold-launch sweep mirror │
│ Backgrounder — class with create / register / start / │
│ schedule / cancel / cancelAll / scheduled / │
│ guarantees / runNow / shutdown │
└────────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
┌───────┴────────────┐ ┌───────────┴──────────┐ ┌───────────────┴────────┐
│ androidMain │ │ iosMain │ │ macosMain │
│ WorkManager- │ │ BGTaskBacked- │ │ NSBackground- │
│ Scheduler │ │ Scheduler │ │ ActivityBacked- │
│ RegistryDispatch │ │ + state store │ │ Scheduler │
│ Worker │ │ + coroutine bridge │ │ │
│ │ │ + foreground/ │ │ │
│ │ │ background feeds │ │ │
│ + WorkManager- │ │ │ │ + LibraryScope- │
│ InstantRunner │ │ + UIBackgroundTask- │ │ InstantRunner │
│ │ │ InstantRunner │ │ │
└────────────────────┘ └──────────────────────┘ └────────────────────────┘
ARM-only native targets — iosArm64, iosSimulatorArm64, Android arm64-v8a, macosArm64 — plus the architecture-neutral jvm target (desktop / server). No Catalyst, no x86 native slices, no watchOS, no tvOS.
Two surfaces, one entry point¶
Backgrounder exposes two parallel dispatch surfaces and a shared cancel:
- Scheduling verbs (
backgrounder.schedule(...),backgrounder.cancelAll(),backgrounder.scheduled(),backgrounder.guarantees()) — OS-backed scheduled work. HonorsWorkConstraints,BackoffPolicy, retries. Workers come from the registry. See Schedule a one-shot / Periodic. runNow<R>(taskId, task)— instant dispatch. Suspends until the typed result is back. Bypasses constraints, backoff, retries, and the registry — the lambda is the work. Routed through the platform's real background primitive (beginBackgroundTaskon iOS,WorkManageron Android, library scope on macOS and the JVM). See Run now.cancel(taskId)onBackgrounderis the unified cancel — it kills both scheduled and in-flightrunNowfor the givenTaskId.cancelAll()covers only pending scheduled requests and does not touch in-flightrunNowcalls.
The two surfaces are independent code paths — a TaskId can flow through either or both. They share only the Backgrounder lifecycle (start() / shutdown()) and the cancel surface above.
Why this shape¶
- One public surface, four actuals. Consumers in
commonMainwrite againstBackgrounder(scheduling verbs +runNow),BackgroundWorker, andBackgroundWorkerFactory. Each platform'sBackgrounder.Companion.create(...)factory wires up its own internal scheduler and runner implementations, using plain constructor injection — no DI container required. - Workers are factory-built per invocation. The library never instantiates a worker by reflection; the user registers a
() -> BackgroundWorkerfactory at app launch and the library calls it each time the platform fires. This is the@HiltWorkermodel generalised for KMP — see Worker context & DI. (runNowis the exception — it uses a caller-supplied lambda directly instead of consulting the registry.) - State lives where the platform owns it. Android persists requests in WorkManager's SQLite; iOS persists library-level retry/state in
NSUserDefaultsviamultiplatform-settings. macOS holds active schedulers in-memory (the OS doesn't need persistence —NSBackgroundActivityScheduleris in-process); the JVM holds its coroutine jobs in-memory the same way (only the ephemeral mirror persists, viajava.util.prefs).runNowis fully in-process — no persistence on any platform. - Guarantees are honest.
Backgrounder.guarantees()returns a per-platform truth table; UX should branch on it rather than assume parity. See Guarantees.runNowmakes a different (weaker) set of guarantees — see Run now.