$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.
See it happen
One-time and recurring work compile to different scheduling calls; a delay adds one more argument to either.
import $lib.components.ui.Button
var syncing = false
fun sync() {
syncing = true
$dispatch(api.syncData, constraints: ["wifi"])
}
<Button onClick={sync}>
<Text>Sync</Text>
</Button>$dispatch compiles to a single scheduleDispatch call, keyed by the constraints you gave it — no WorkRequest builder to write by hand.
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 } Build errors
Each of these is refused at build time rather than failing on a device: an unknown
constraint, an unknown duration unit, an argument the call does not take, "expedited" together with a delay, and "expedited" on $periodic.
Work Status
Track work progress:
fun startUpload() {
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
@prop val photoId: String
var upload: DispatchHandle? = $state(null)
fun checkUpload() {
upload = $work("upload-${photoId}")
}
<Column>
<Button onClick={checkUpload}>
<Text>Check status</Text>
</Button>
@if (upload != null) {
<Text>Status: {upload?.status}</Text>
<Button onClick={() => upload?.cancel()}>
<Text>Cancel</Text>
</Button>
}
</Column> Run Periodic Work Immediately
$onMount {
$periodic(api.syncData, every: { hours: 1 }, name: "sync")
}
fun runNow() {
$work("sync")?.runNow()
}
<Button onClick={runNow}>
<Text>Sync Now</Text>
</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
$onMount {
$periodic(api.syncData, every: { hours: 1 }, name: "background-sync", constraints: ["wifi"])
}
fun syncNow() {
$work("background-sync")?.runNow()
}
fun cancelSync() {
$work("background-sync")?.cancel()
}
<Column class="gap-16 p-16">
<Button onClick={syncNow}>
<Text>Sync Now</Text>
</Button>
<Button onClick={cancelSync}>
<Text>Cancel Background Sync</Text>
</Button>
</Column> Photo Upload Example
fun uploadPhoto(photoId: String) {
$dispatch(api.uploadPhoto, name: "upload-$photoId", constraints: ["wifi", "batteryOk"])
}
// Check upload status
fun isUploading(photoId: String): Boolean {
return $work("upload-$photoId")?.status == "running"
} Grouping several calls
A block runs as one unit of work:
$dispatch {
api.sync()
api.clean()
} Behind the Scenes
WorkManager without a Worker class — the work is the call as written. A name makes it unique; periodic work is always named, after its file and call
if you do not name it yourself, since Android's own minimum is fifteen minutes and a
duplicate would just collide with itself.
The work is a lambda living in the app's own process. Work still pending when the process
ends fails, with a log line rather than a retry you did not ask for. $periodic survives that by being called again at startup — which is why a root layout or a store,
not a screen, is where it belongs.
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