Skip to content

Changelog

Unreleased

Instant dispatch — Backgrounder.runNow

  • New suspend fun <R> Backgrounder.runNow(taskId, task): R for "run this lambda in the background right now and let me await the typed result." Complements scheduled work — bypasses WorkConstraints, BackoffPolicy, retries, and the BackgroundWorker / register path entirely; the lambda is the work. See Run now.
  • Routed through the platform's real background primitive so the work survives if the caller backgrounds mid-call:
    • Android: WorkManager (a synthetic one-time request keyed ${taskId}::runNow).
    • iOS: UIApplication.beginBackgroundTask(withName:expirationHandler:)not BGTaskScheduler. BGTaskScheduler requires Info.plist permitted-identifiers and is for deferred work; beginBackgroundTask grants ~30s of grace if the app backgrounds during the call, with no Info.plist requirement. The TaskId is purely an in-process pre-emption key on iOS — never sent to the OS scheduler.
    • macOS: library-owned SupervisorJob scope (macOS apps generally have foreground time; NSBackgroundActivityScheduler is interval-shaped and a poor fit for one-shot dispatch).
  • Pre-emption is the contract. runNow(taskId, …) cancels any in-flight runNow, any pending scheduled request, and any in-flight scheduled worker for the same TaskId before submitting its own request. Concurrent runNow calls with the same TaskId are "last call wins" — two typed results to one caller would be ambiguous.
  • Unified Backgrounder.cancel(taskId) cancels everything for a TaskId — scheduled requests and in-flight runNow. Scheduler.cancel(taskId) keeps its narrow scheduled-only meaning.
  • Structured concurrency throughout: caller cancellation cancels the OS request, the lambda observes CancellationException, and the caller's await rethrows. Lambda exceptions propagate to the caller via @Throws.

iOS periodic dispatch

  • WorkRequest.Periodic is now driven by a coalescing dispatcher with two feeds — an in-process loop while the app is foregrounded, and a single library-owned BGAppRefreshTaskRequest while it is not. iOS suppresses BGAppRefreshTaskRequest for foregrounded apps, so the in-process loop is what fires periodics at the right moment during user sessions; without it a periodic whose interval elapsed during a long session would silently slip past until the user backgrounded the app.
  • Foreground-initiated dispatch is wrapped in UIApplication.beginBackgroundTaskWithName runway so work that gets backgrounded mid-execution gets the OS-granted continuation window before being treated as Retry.
  • Coalescing-by-TaskId is an explicit cross-platform contract (documented on WorkRequest.Periodic and in iOS launch sequence). If iOS doesn't dispatch for several intervals, the worker fires once on the next wake — never N times back-to-back to "catch up." Workers that need catch-up logic compute it from their own persisted state (e.g. lastSyncedAt).
  • Backgrounder.create(tickIdentifier:) takes a required tick identifier on iOS. Pick a string in your app's reverse-DNS namespace (e.g. "<your.bundle.id>.background-tick") and add it to BGTaskSchedulerPermittedIdentifiers in Info.plist. Periodic task ids do not need their own Info.plist entries — the tick handles them. One-shot task ids (WorkRequest.OneTime) still register per-id and still need their own entries.
  • WorkConstraints on WorkRequest.Periodic are not honored on iOS — App Refresh has no constraint fields; the in-process loop has no constraint concept. Workers that need power/network gating should check at the start of execute() and return WorkResult.Retry. WorkConstraints are honored for WorkRequest.OneTime on iOS, and on Android / macOS for both kinds.

v1 surface

First public artifact in preparation. The v1 surface is feature-complete:

  • Constructed-instance Backgrounder entry point. Three-step launch: Backgrounder.create(...)register(taskId, factory)start(). Hold one instance per app for the lifetime of the process.
  • No DI container required. Factory closures resolve dependencies from whatever DI graph the consumer already uses (Koin, Hilt, kotlin-inject, hand-wired); the library itself ships zero DI dependency.
  • Cross-platform Scheduler API with schedule / cancel / cancelAll / scheduled() / guarantees(). Reach via backgrounder.scheduler.
  • Sealed WorkRequest: OneTime and Periodic, both with ephemeral flag.
  • BackoffPolicy (Linear / Exponential) with maxAttempts.
  • ExecutionHint: Standard and Expedited(QuotaPolicy).
  • WorkInput typed key/value bag, capped at 10 240 bytes.
  • Cold-launch ephemeral sweep with platform-appropriate timing + per-instance Android ready-gate backstop.
  • iOS periodic emulation via library-internal state machine, with force-quit resurrection.
  • macOS native periodic via NSBackgroundActivityScheduler.
  • Per-platform SchedulerGuarantees for honest UX branching.
  • Android: hand-rolled BackgrounderWorkerFactory that consumers install via Configuration.Provider.workManagerConfiguration. Composes with Hilt's HiltWorkerFactory via DelegatingWorkerFactory.
  • MkDocs Material documentation site with Dokka API reference.

v2 roadmap (not yet)

  • Reactive Scheduler.observe() Flow (cross-platform).
  • ExecutionHint.LongRunning for Android setForeground / foreground-service work.
  • Android-only constraints (storage-not-low, device-idle, content URI triggers) in a WorkConstraints.Android extension.
  • Published :testing artifact with stable, public FakeScheduler.

The latest version of this changelog lives at the GitHub Releases page once published.