Skip to content

feat: add requiresDeviceIdle constraint + opportunistic-dispatch docs#37

Merged
happycodelucky merged 1 commit into
mainfrom
happycodelucky/laughing-montalcini-8d43ea
Jun 20, 2026
Merged

feat: add requiresDeviceIdle constraint + opportunistic-dispatch docs#37
happycodelucky merged 1 commit into
mainfrom
happycodelucky/laughing-montalcini-8d43ea

Conversation

@happycodelucky

Copy link
Copy Markdown
Owner

What

Adds a new cross-platform work constraint, WorkConstraints.requiresDeviceIdle, and documents the library's opportunistic-dispatch contract — both in response to the question "how do we support 'run this when the OS gives the app space', and what is the Android equivalent of iOS background fetch?"

Two deliverables:

  1. requiresDeviceIdle: Boolean = false — "only dispatch when the device is idle." It was previously parked as a v2 gap in the WorkConstraints KDoc; it now ships.
  2. docs/concepts/opportunistic-dispatch.md — names the contract that already governs every platform (intervals are floors, not promises; the OS picks the moment) and pre-answers the recurring "why didn't my background task run on time" confusions.

Why

The "do this when the OS gives you space" behaviour was already the library's only mode — BGTaskScheduler / WorkManager / NSBackgroundActivityScheduler all dispatch opportunistically. What was missing was (a) a documented name for that contract so callers stop reading interval as a wall-clock promise, and (b) the Android-idle constraint, which is the concrete "idle?" analogue the iOS background-fetch model implies.

Platform behaviour (the honest table)

requiresDeviceIdle follows the same advisory-constraint template as requiresCharging:

Platform Behaviour Mechanism
Android Enforced Constraints.Builder.setRequiresDeviceIdle(true)JobScheduler (Doze / maintenance windows). Surfaces as PendingPredicate.RequiresDeviceIdle via scheduled().
iOS Advisory (no-op) BGProcessingTaskRequest has no idle property (only requiresExternalPower / requiresNetworkConnectivity); the system already prefers idle moments. One-time log.w, mirroring the existing NetworkRequirement.Unmetered warning.
macOS Advisory (no-op) NSBackgroundActivityScheduler has no idle primitive; already biases idle via qualityOfService = .background. Silent-with-comment.
JVM Advisory (no-op) No OS idle concept. Silent-with-comment.

Reviewer notes

  • @Serializable compatibility: new field has a default (= false) and is appended (existing field order unchanged) — serialization/binary compatible.
  • Inspector parity: PendingPredicate.RequiresDeviceIdle + Android WorkInfo read-back were wired at every touch-point that RequiresCharging already occupies (field → AndroidConstraintsMapperAndroidScheduledTaskMapper predicate + snapshot data class → PendingPredicate), to avoid the duplicate-drift trap this repo has hit before.
  • macOS/JVM deliberately log nothing. Those files already treat requiresCharging as silent-with-a-doc-note; I matched the local convention rather than inventing a log they don't have. iOS does log because its applyConstraints already logs for Unmetered.
  • No WorkerContext / clock changes. This adds no timing logic — N-011 (no wall-clock in scheduler math) and N-012 (no WorkerContext singleton proxy) are not touched.
  • minInterval / last-run throttling was considered and descoped by the owner this session — opportunistic dispatch already floors execution frequency, so a throttle would be redundant machinery. Captured in LESSONS D-025 so it isn't re-proposed.
  • Docs are strict-mode clean: the new concept page deep-links into a new network-required.md#require-the-device-to-be-idle anchor and is wired into mkdocs.yml nav; mkdocs build --strict + the repo's docs:check both pass.

Verification

  • ./gradlew checkPASS (82 tasks; all unit tests across JVM/Android/macOS/iOS-sim; no ktlint/detekt failures)
  • ./gradlew :backgrounder:linkDebugFrameworkIosArm64PASS (iOS framework links clean)
  • mise run docs:check + mkdocs build --strictPASS

🤖 Generated with Claude Code

Add `WorkConstraints.requiresDeviceIdle` (previously parked as a v2 gap)
and document the library's opportunistic-dispatch contract.

requiresDeviceIdle is enforced natively on Android via
`Constraints.Builder.setRequiresDeviceIdle` (JobScheduler / Doze
maintenance windows) and surfaces as `PendingPredicate.RequiresDeviceIdle`
through `scheduled()`. On iOS/macOS/JVM it is advisory: those systems have
no device-idle knob (`BGProcessingTaskRequest` exposes only
requiresExternalPower / requiresNetworkConnectivity) and already prefer
idle moments themselves — iOS logs once, macOS/JVM stay silent-with-a-
comment to match how `requiresCharging` is already treated in those files.
This establishes the advisory-constraint template: enforce where native,
accept-and-document elsewhere, never imitate (LESSONS D-025).

Docs: new concepts/opportunistic-dispatch.md names the "interval is a
floor, not a promise" contract and pre-answers the common "why didn't my
background task run on time" confusions. Per-platform constraint honesty
notes updated across android/ios/macos/jvm and the network-required recipe.

minInterval/last-run throttling was considered and descoped — opportunistic
dispatch already floors execution frequency.

Verified: ./gradlew check + linkDebugFrameworkIosArm64 pass; mkdocs strict
build and docs:check pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@happycodelucky happycodelucky merged commit 2257144 into main Jun 20, 2026
2 checks passed
@happycodelucky happycodelucky deleted the happycodelucky/laughing-montalcini-8d43ea branch June 20, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant