Changelog¶
Unreleased¶
Built with Kotlin 2.4¶
- The library is now compiled with Kotlin 2.4.10 at language and API version 2.4. Kotlin Multiplatform consumers need Kotlin 2.4 or newer: a klib can only be read by a compiler at least as new as its language version. JVM and Android consumers need Kotlin 2.3 or newer. Swift / SPM consumers are unaffected.
- No public API change. The Kotlin ABI (klib and JVM) and the Swift / Objective-C headers are identical to the previous build.
- The XCFramework's built-in minimum OS moves from iOS 14 / macOS 11 to iOS 15 / macOS 12, Kotlin 2.4's new default.
Package.swiftalready requires iOS 18 / macOS 15, so nothing changes for SPM consumers. kotlinx-datetimeandkotlinx-collections-immutableare no longer runtime dependencies; the library never used them. If your app relied on getting them transitively, declare them yourself.
Docs: API reference is linked and complete¶
- The Dokka API reference now has an API reference tab in the site navigation and a link from the Overview page. It previously built to an empty "All modules" page (the root project's Gradle coordinates collided with
:backgrounder's, so the aggregate resolved to itself); it now coversbackgrounderandbackground-monitor.
Backgrounder class renamed to BackgroundTaskManager¶
- The entry-point class is now
BackgroundTaskManager;Backgrounderremains the library, Maven group, framework module, and Gradle plugin name. Two reasons: the class name now says what the object is, and the old name collided with the Apple framework module, which made SKIE expose it to Swift asBackgrounder_. Swift now readsBackgroundTaskManager.sharedafterimport Backgrounder. Library-branded siblings (BackgrounderEventListener,BackgrounderInitializer,BackgrounderWorkerFactory) keep their names.
One BackgroundTaskManager per process: BackgroundTaskManager.shared¶
- There is now exactly one live
BackgroundTaskManagerper process, reachable anywhere asBackgroundTaskManager.shared(Kotlin and Swift). Nothing needs to be injected or passed around;single { BackgroundTaskManager.shared }if you want it in a DI graph. A second live instance throws;shutdown()releases the slot. - Android: the library's manifest registers
BackgrounderInitializerwithandroidx.startup, sosharedexists beforeApplication.onCreate.BackgroundTaskManager.create(application)is renamedBackgroundTaskManager.configure(application)and is only needed by apps that removed theInitializationProvideror run in extra processes; it is idempotent with the initializer.workManagerConfigurationreadsBackgroundTaskManager.shared.androidWorkerFactory(). - iOS / macOS / JVM:
sharedbuilds itself on first access.create(...)remains for apps that want an event listener or, on iOS, their own tick identifier, and installs its result asshared. - iOS default tick identifier:
<bundle id>.backgrounder-tick, returned byBackgroundTaskManager.companion.defaultTickIdentifier(). The Gradle plugin adds it to the plist whenbackgrounder.iosBundleIdentifieris set. - Ephemeral sweep timing: on every platform the leftover ids are snapshotted when the instance is built and cancelled at
start(). On Android this moves the sweep out of construction (which may now run beforeonCreate) and means an ephemeral request scheduled between construction andstart()is never mistaken for a leftover. - Swift gets
BackgroundTaskManager.sharedthrough a wrapper bundled into the framework by SKIE; the raw bridge function isBackgroundTaskManager.companion.sharedInstance().
Gradle plugin: generated BGTaskSchedulerPermittedIdentifiers¶
- New
@BGTaskSchedulerPermittedIdentifierannotation in:backgrounder. Put it on theconst val Stringids that belong in the iOSBGTaskSchedulerPermittedIdentifiersarray — the tick identifier and any id you may schedule as aOneTime— wherever they live: top level,object, orcompanion object. iOS-only; periodic andrunNowids don't need it, and annotating them is harmless. - New
com.happycodelucky.backgrounderGradle plugin (:backgrounder-gradle-plugin, published to Maven Central).collectBackgroundTaskIdsscans the module's compiled JVM classes for annotated constants and writes a manifest;updateBackgrounderInfoPlistrewrites theBGTaskSchedulerPermittedIdentifiersarray in the configuredInfo.plistfrom it, touching nothing else in the file. Duplicate, blank, or non-constids fail the build; non-reverse-DNS ids warn. See Generate the iOS permitted identifiers. - The plugin has no dependency on the Kotlin Gradle plugin or on
:backgrounder; it finds the compile task by name and matches the annotation by descriptor.
Task ids are plain strings¶
TaskIdis gone. Every API that took aTaskIdnow takes aString:register,WorkRequest.OneTime/Periodic,runNow,cancel,WorkerContext.taskId,ScheduledTask.taskId,BackgroundWorkerFactory.taskIds,FactoryDescriptor, andMonitorEvent. Swift already saw these asString(Kotlin/Native exports a value class as its underlying type), so the Kotlin and Swift surfaces now match.- The reverse-DNS shape is no longer enforced. Ids are free-form; reverse-DNS is the documented convention. See Task ids.
- The remaining rules, non-blank with no surrounding whitespace and no control characters, is checked at every public entry point instead of in a constructor, so Swift callers are validated too.
BackgroundTaskManager.create(tickIdentifier:)now validates the tick identifier for the first time and is annotated@Throws(IllegalArgumentException::class);runNowgainsIllegalArgumentExceptionin its@Throwslist. - Persisted state is unaffected:
TaskIdserialized as its underlying string, so stored schedules keep loading.
Instant dispatch — BackgroundTaskManager.runNow¶
- New
suspend fun <R> BackgroundTaskManager.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. The task id is 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 same task id before submitting its own request. ConcurrentrunNowcalls with the same task id are "last call wins" — two typed results to one caller would be ambiguous. - Unified
BackgroundTaskManager.cancel(taskId)cancels everything for a task id — scheduled requests and in-flightrunNow.BackgroundTaskManager.cancelAll()covers only pending scheduled requests and does not touch in-flightrunNowcalls. - 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-task-id is an explicit cross-platform contract (documented on
WorkRequest.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). BackgroundTaskManager.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
BackgroundTaskManagerentry point. Three-step launch:BackgroundTaskManager.create(...)→register(...)→start(). Hold one instance per app for the lifetime of the process. - Two registration shapes. Per-id:
register(taskId) { factory }for a single task id. Bulk:register(factory: BackgroundWorkerFactory)for one factory object that owns many task ids. Overlapping id sets are rejected at registration time; resolution order is per-id first, then factories in registration order. - 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.
- Scheduling verbs promoted directly onto
BackgroundTaskManager:schedule/cancel/cancelAll/scheduled()/guarantees(). Pass theBackgroundTaskManagerinstance wherever scheduling is needed — no separateSchedulerhandle. - 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.