Documentation

$dispatch / $periodic

Schedule background work that survives app restarts.

$dispatch - One-Time Work

Run a suspend function in the background:

// Simple dispatch
$dispatch(api.syncData)

// With constraints
$dispatch(api.uploadPhotos, constraints: ["wifi"])

// With delay
$dispatch(api.sendReminder, delay: { mins: 5 })

// Named (for later retrieval)
$dispatch(api.uploadPhoto, name: "upload-${photoId}")

$periodic - Recurring Work

Run work periodically:

// Run every hour
$periodic(api.refreshFeed, every: { hours: 1 })

// With constraints and name
$periodic(
  api.syncData,
  every: { mins: 60 },
  name: "data-sync",
  constraints: ["wifi", "charging"]
)

Android enforces a minimum 15-minute interval for periodic work.

Constraints

Specify conditions for work execution:

  • "network" - Any network connection
  • "wifi" - WiFi connection required
  • "batteryOk" - Battery not low
  • "charging" - Device charging
  • "idle" - Device idle (Doze mode)
  • "storageOk" - Storage not low
  • "expedited" - Run as soon as possible
$dispatch(
  api.uploadVideo,
  constraints: ["wifi", "charging", "batteryOk"]
)

Duration Syntax

delay: { hours: 1 }
delay: { mins: 30 }
delay: { seconds: 45 }
delay: { hours: 1, mins: 30 }

every: { hours: 2 }
every: { mins: 60 }

Work Status

Track work progress:

val upload = $dispatch(api.uploadFile, name: "upload-123")

// Check status
val status = upload.status
// "pending" | "running" | "succeeded" | "failed" | "cancelled"

// Cancel work
upload.cancel()

Retrieve Work by Name

val upload = $work("upload-${photoId}")

@if (upload != null) {
  <Text>Status: {upload.status}</Text>
  <Button onClick={() => upload.cancel()}>Cancel</Button>
}

Run Periodic Work Immediately

val sync = $periodic(api.syncData, every: { hours: 1 }, name: "sync")

// Force run now (doesn't affect schedule)
<Button onClick={() => sync.runNow()}>
  Sync Now
</Button>

Sequential Work

Wait for work to complete before starting the next:

suspend fun syncAll() {
  $await $dispatch(api.syncUsers)
  $await $dispatch(api.syncOrders)
  $await $dispatch(api.syncProducts)
}

Complete Example

// Background sync with UI control
var syncStatus = "idle"

val sync = $periodic(
  {
    syncStatus = "syncing"
    api.syncData()
    syncStatus = "done"
  },
  every: { hours: 1 },
  name: "background-sync",
  constraints: ["wifi"]
)

<Column class="gap-16 p-16">
  <Text>Status: {syncStatus}</Text>
  <Text>Last sync: {sync.status}</Text>

  <Button onClick={() => sync.runNow()}>
    Sync Now
  </Button>

  <Button onClick={() => sync.cancel()}>
    Cancel Background Sync
  </Button>
</Column>

Photo Upload Example

suspend fun uploadPhoto(photoId: String, uri: String) {
  $dispatch(
    {
      val file = readFileFromUri(uri)
      $fetch.post(
        url = "https://api.example.com/photos",
        body = file
      )
    },
    name: "upload-$photoId",
    constraints: ["wifi", "batteryOk"],
    delay: { seconds: 0 }
  )
}

// Check upload status
val upload = $work("upload-$photoId")
@if (upload?.status == "running") {
  <CircularProgressIndicator />
}

Behind the Scenes

Whitehall uses WorkManager for background work:

  • Work survives app restarts
  • Respects system constraints (battery, network, etc.)
  • Guaranteed execution (eventually)
  • Handles retries automatically

Use workers for operations that should complete even if the app is closed, like uploads, sync, and scheduled tasks. For immediate work, use regular coroutines with $onMount or launch.

See Also