KMP WorkManager

repository·main·Indexed 42 days ago

https://github.com/brewkits/kmpworkmanager

A background task scheduling library for Kotlin Multiplatform projects that bridges Android's WorkManager and iOS's BGTaskScheduler. It supports persistent, chainable background workflows, type-safe serialization for worker input, and automatic WorkerFactory generation via a KSP processor (version 2.4.3). Features include support for OneTime and Periodic tasks, task chaining, and platform-specific triggers like ContentUri and DeviceIdle on Android.

Tokens
92.3K
Snippets
243
Records
332
Agent score
49%

What's inside KMP WorkManager

  1. Understand iOS background task limitations and mindset shifts

    main

    When migrating from Android to iOS using KMP WorkManager, you must shift from a 'guaranteed execution' mindset to an 'opportunistic execution' mindset. iOS background tasks are subject to system-decided timing and have strict constraints compared to Android.

    Key Differences

    • Timing: Android tasks are predictable (within a flex window); iOS tasks are unpredictable and run when the system deems it appropriate.
    • Duration: Android allows long-running tasks (minutes); iOS tasks are limited to approximately 30-60 seconds maximum.
    • Persistence: Android tasks continue even if the app is closed; on iOS, if the user force-quits the app, all background tasks are killed immediately.
    • Constraints: Android supports rich constraints (Charging, Battery Not Low, Idle, etc.). iOS only supports basic network connectivity constraints; all other conditions must be checked manually within the worker.
  2. Ensure ProGuard and Rename safety for Workers

    main

    If you do not provide an explicit name to the @Worker annotation, KSP defaults to the simple class name. This is dangerous because:

    1. ProGuard/R8 might obfuscate the class name.
    2. Refactoring the class name will change the lookup key.

    If a task is already persisted in the system queue under the old name, it will fail to run (resulting in a "worker not found" error).

    Best Practice: Always provide a stable string name.

    @Worker(name = "SyncWorker") // Stable across renames and ProGuard
    class SyncWorker : AndroidWorker { ... }
  3. How chain abandonment works on iOS (v2.5.0)

    main

    In v2.5.0, ChainExecutor on iOS now honors the shouldRetry flag in WorkerResult.Failure.

    • Failure(shouldRetry = false): Triggers immediate chain abandonment. The chain definition and progress files are deleted immediately.
    • Failure(shouldRetry = true) or Retry(...): Continues to consume the chain-level retry budget (default MAX_RETRIES = 3).

    Warning: Because the default value for shouldRetry in the Failure data class is false, any worker returning WorkerResult.Failure("message") without explicitly setting shouldRetry = true will now cause the entire chain to abort on the first attempt. If you want a transient failure to trigger a retry, you must use WorkerResult.Failure(message, shouldRetry = true) or WorkerResult.Retry(reason).

  4. Handle Android 15 `dataSync` 6-hour runtime cap

    main

    Android 15 enforces a cumulative 6-hour-per-day cap on dataSync foreground service runtime. If this cap is reached:

    • setForeground() throws ForegroundServiceTypeException.
    • WorkManager marks the work as FAILED and will not retry.

    Recommendation: For long-running tasks (like large backups), use chunked work combined with the requiresUnmeteredNetwork constraint so that each worker instance remains short-lived.

  5. Understand ExecutionRecord and ExecutionStatus

    main

    An ExecutionRecord is a persisted record of a background task chain execution, written when a chain finishes.

    ExecutionStatus values:

    • SUCCESS: All steps completed successfully.
    • FAILURE: A step failed; the chain is re-queued for retry.
    • ABANDONED: Max retries exhausted or non-idempotent tasks after corrupt-progress self-heal.
    • SKIPPED: Chain discarded before execution (due to REPLACE policy or deadline exceeded).
    • TIMEOUT: Chain timed out within its BGTask window; re-queued for continuation.
  6. What are Task Chains in KMP WorkManager?

    main

    Task chains allow you to orchestrate complex background workflows by executing multiple tasks in a specific order. They support both sequential execution (one task after another) and parallel execution (multiple tasks simultaneously).

    Key features include:

    • Automatic dependency management: Tasks run in the correct order based on the chain definition.
    • Error handling: If any task in the chain fails, the entire chain stops.
    • Parallel execution: Independent tasks can run at the same time to optimize performance.
    • Type-safe builder API: Uses a fluent syntax for defining workflows.
  7. Understand iOS Background Task limits and constraints

    main

    When using kmpworkmanager on iOS, you must account for the operating system's strict limits on background execution. iOS background tasks are opportunistic, meaning the OS decides when to run them, not the application.

    Key Constraints

    LimitHard ceilingLibrary can mitigate?Workaround
    Opportunistic schedulingOS decides, not usNoDon't use BGTask for alarms/realtime; use UNUserNotification or silent push
    Time budget~30s App Refresh, few min ProcessingPartial (checkpointing, expirationHandler)Granular steps ≤ 5s; prefer BGProcessingTaskRequest
    Headless DI cold-start~10s before budget starts tickingNoLazy-init non-BGTask deps; measure cold-start on real devices
    No ZIP codecK/N stdlib gapYes (fail-fast default)Use Swift host for compression, or wait for v2.6 zlib cinterop
    Exact alarmsiOS has no "wake at time T" primitivePartial (catch-up on app open)Use UNUserNotification for user-visible alarms; server-side scheduler for SLA-critical timing
    Master dispatcherAlways BGProcessingTaskRequest (25s timeout)NoUse a dedicated static Info.plist ID for light tasks
    Simulator SupportBGTaskScheduler is unavailablePartial (falls back to delay(), logs a WARN)Verify background execution on hardware

    Design Philosophy

    If your feature breaks when iOS decides not to wake the app, it should not be implemented as a BGTask. Use BGTask for opportunistic, best-effort work where "eventually" is acceptable.

  8. Understand platform differences for task chains

    main

    KMP WorkManager behaves differently on Android and iOS due to underlying system limitations.

    Android

    Uses the native WorkManager continuation API.

    • Features: Full support for sequential/parallel chains, automatic retries with backoff, persistence across reboots, and respect for Doze mode.
    • Limitations: Minimum 15-minute interval for periodic chains; tasks may be delayed by system battery optimizations.

    iOS

    Uses a custom coroutine-based queue system.

    • Features: Supports sequential/parallel execution and batch execution (up to 3 tasks at once).
    • Limitations:
      • BGAppRefreshTask has a 25-second timeout per task.
      • BGProcessingTask allows several minutes if isHeavyTask = true is set in constraints.
      • Tasks only run when the app is in the background and execution is opportunistic (decided by iOS).

    iOS Best Practices

    • Keep chains short (ideally 3-5 tasks).
    • Use isHeavyTask = true in Constraints for long-running chains.
    • Break very long chains into multiple periodic tasks.
    • Always test on physical devices, as simulator behavior differs.
  9. Use ParallelHttpDownloadWorker for faster downloads

    main

    The ParallelHttpDownloadWorker improves download speed by splitting a file into multiple HTTP Range chunks and downloading them concurrently.

    Key features:

    • Concurrency: Supports splitting a file into $N$ chunks, where $N$ is between 1 and 16 (default is 4).
    • Resumability: Persists .partN files for each chunk, allowing individual chunks to resume after a process kill or retry.
    • Fallback: Automatically falls back to sequential downloading if the server does not support Accept-Ranges: bytes or does not provide a Content-Length header.
    • Merging: Once all chunks are complete, they are merged into the final file.
    // Conceptual usage of ParallelHttpDownloadWorker
    // It uses HTTP Range headers to download chunks concurrently.
  10. Handle iOS background downloads with IosBackgroundDownloadWorker

    main

    The IosBackgroundDownloadWorker (experimental) allows downloads to survive full app termination by utilizing URLSessionConfiguration.background.

    Lifecycle Note: The worker returns Success as soon as the iOS operating system accepts the request. The actual completion of the download is delivered later via the TaskEventBus. For host integration details, see docs/IOS_BACKGROUND_URL_SESSION.md.

  11. Use TaskChain to sequence or parallelize tasks

    main

    A TaskChain allows you to define a sequence of tasks that execute one after another, or groups of tasks that execute in parallel. You initiate a chain using a BackgroundTaskScheduler and build it using the .then() method. Once the chain is defined, call .enqueue() to schedule it.

    • Sequential Execution: Use .then(request: TaskRequest) to add a single task that runs after the previous one completes.
    • Parallel Execution: Use .then(requests: List<TaskRequest>) to add a group of tasks that will run in parallel after the previous step in the chain completes.
    • Mixed Chains: You can combine these methods to create complex workflows where some steps are sequential and others are parallel.
    // Example of a mixed chain
    backgroundTaskScheduler
        .beginWith(TaskRequest(workerClassName = "StepOneWorker"))
        .then(listOf(
            TaskRequest(workerClassName = "ParallelWorkerA"),
            TaskRequest(workerClassName = "ParallelWorkerB")
        ))
        .then(TaskRequest(workerClassName = "FinalWorker"))
        .enqueue()
  12. How iOS background tasks retry (v2.5.0)

    main

    In v2.5.0, both dedicated-identifier tasks and dynamic tasks on iOS follow the standard WorkManager retry contract. Workers must be idempotent because they may run up to 5 times by default.

    Retry Contract:

    • Success $\rightarrow$ Task completed, do not re-enqueue.
    • Failure(shouldRetry = false) $\rightarrow$ Terminal failure, do not re-enqueue.
    • Failure(shouldRetry = true) $\rightarrow$ Re-enqueue with incremented attempt counter.
    • Retry(attemptCap = N) $\rightarrow$ Re-enqueue, abandon after N total attempts.
    • Retry(attemptCap = null) $\rightarrow$ Re-enqueue, abandon after 5 attempts (default).
    • Retry(delayMs = X) $\rightarrow$ Re-enqueue with earliestBeginDate = now + X.

    Note: The attempt counter is persisted under the internal kmpAttemptCount key to survive process kills.