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
- $fetch - HTTP requests
- $onMount / $onDispose - Lifecycle hooks
- $notify - Notifications