Changelog¶
Unreleased¶
Instant dispatch — Backgrounder.runNow¶
- New
suspend fun <R> Backgrounder.runNow(taskId, task): Rfor "run this lambda in the background right now and let meawaitthe typed result." Complements scheduled work — bypassesWorkConstraints,BackoffPolicy, retries, and theBackgroundWorker/registerpath 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:)— notBGTaskScheduler.BGTaskSchedulerrequiresInfo.plistpermitted-identifiers and is for deferred work;beginBackgroundTaskgrants ~30s of grace if the app backgrounds during the call, with noInfo.plistrequirement. TheTaskIdis purely an in-process pre-emption key on iOS — never sent to the OS scheduler. - macOS: library-owned
SupervisorJobscope (macOS apps generally have foreground time;NSBackgroundActivityScheduleris interval-shaped and a poor fit for one-shot dispatch).
- Android:
- Pre-emption is the contract.
runNow(taskId, …)cancels any in-flightrunNow, any pending scheduled request, and any in-flight scheduled worker for the sameTaskIdbefore submitting its own request. ConcurrentrunNowcalls with the sameTaskIdare "last call wins" — two typed results to one caller would be ambiguous. - Unified
Backgrounder.cancel(taskId)cancels everything for aTaskId— scheduled requests and in-flightrunNow.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'sawaitrethrows. Lambda exceptions propagate to the caller via@Throws.
iOS periodic dispatch¶
WorkRequest.Periodicis now driven by a coalescing dispatcher with two feeds — an in-process loop while the app is foregrounded, and a single library-ownedBGAppRefreshTaskRequestwhile it is not. iOS suppressesBGAppRefreshTaskRequestfor 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.beginBackgroundTaskWithNamerunway so work that gets backgrounded mid-execution gets the OS-granted continuation window before being treated asRetry. - Coalescing-by-
TaskIdis an explicit cross-platform contract (documented onWorkRequest.Periodicand 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 toBGTaskSchedulerPermittedIdentifiersinInfo.plist. Periodic task ids do not need their ownInfo.plistentries — the tick handles them. One-shot task ids (WorkRequest.OneTime) still register per-id and still need their own entries.WorkConstraintsonWorkRequest.Periodicare 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 ofexecute()and returnWorkResult.Retry.WorkConstraintsare honored forWorkRequest.OneTimeon iOS, and on Android / macOS for both kinds.
v1 surface¶
First public artifact in preparation. The v1 surface is feature-complete:
- Constructed-instance
Backgrounderentry 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
SchedulerAPI withschedule/cancel/cancelAll/scheduled()/guarantees(). Reach viabackgrounder.scheduler. - Sealed
WorkRequest:OneTimeandPeriodic, both withephemeralflag. BackoffPolicy(Linear / Exponential) withmaxAttempts.ExecutionHint:StandardandExpedited(QuotaPolicy).WorkInputtyped 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
SchedulerGuaranteesfor honest UX branching. - Android: hand-rolled
BackgrounderWorkerFactorythat consumers install viaConfiguration.Provider.workManagerConfiguration. Composes with Hilt'sHiltWorkerFactoryviaDelegatingWorkerFactory. - MkDocs Material documentation site with Dokka API reference.
v2 roadmap (not yet)¶
- Reactive
Scheduler.observe()Flow (cross-platform). ExecutionHint.LongRunningfor AndroidsetForeground/ foreground-service work.- Android-only constraints (storage-not-low, device-idle, content URI triggers) in a
WorkConstraints.Androidextension. - Published
:testingartifact with stable, publicFakeScheduler.
The latest version of this changelog lives at the GitHub Releases page once published.