One API. Lock Screen, Dynamic Island, and Android Live Updates.
Start, update and end a live activity from commonMain. iOS renders it with
ActivityKit on the Lock Screen and in the Dynamic Island; Android renders it as an
Android 16 Live Update (promoted ongoing notification with progress and a ticking
chronometer), falling back to a plain ongoing notification on older versions.
| iOS Lock Screen | iOS ended | Android 16 Live Update | Android ended |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
The default iOS widget and the Android notification are both driven by the same
LiveActivityContent: title, subtitle, icon, accent colour, progress and a timer that
ticks natively on each platform without per-second updates.
implementation("io.github.androidpoet:halo:0.2.0") // core API + platform managers
implementation("io.github.androidpoet:halo-compose:0.2.0") // rememberLiveActivityManager()val liveActivities = rememberLiveActivityManager()
val now = Clock.System.now().toEpochMilliseconds()
val request =
LiveActivityRequest(
content =
LiveActivityContent(
title = "Focus session",
subtitle = "Deep work",
progress = LiveActivityProgress.Determinate(current = 0, max = 25),
timer = LiveActivityTimer(startAtEpochMillis = now, endAtEpochMillis = now + 25 * 60_000L),
icon = "timer",
accentColorArgb = 0xFF4F46E5,
),
deepLink = "myapp://focus",
)
when (val result = liveActivities.start(request)) {
is LiveActivityResult.Success -> sessionId = result.value.id
is LiveActivityResult.Failure -> show(result.error.code, result.error.message)
}
liveActivities.update(sessionId, content.copy(subtitle = "Almost there"))
liveActivities.end(sessionId, finalContent = content.copy(title = "Session complete"))The timer ticks on its own on both platforms. Call update only when the content changes.
| Platform | Surface | Floor | One-time setup |
|---|---|---|---|
| Android | Live Update (promoted ongoing notification) on Android 16; ongoing notification below | API 26 | Request POST_NOTIFICATIONS on API 33+ |
| iOS | Lock Screen + Dynamic Island via ActivityKit | iOS 16.2 | Link the Swift package, add the widget extension |
| JVM, macOS, Wasm | UnsupportedLiveActivityManager so shared code compiles |
— | — |
Halo.androidConfig =
AndroidLiveActivityConfig(
smallIconRes = R.drawable.ic_timer,
iconResolver = { key -> if (key == "timer") R.drawable.ic_timer else 0 },
)Icons are resolved through iconResolver at compile time, never by resource name, so
shrunk release builds keep working. AndroidLiveActivityManager.canPromote and
isPromotable(id) tell you whether the system will show the status-bar chip.
ActivityKit is Swift-only and renders in a widget extension, so two things live on the Swift side. Neither is code you write against the Kotlin framework:
- Swift package
swift/HaloKMP— add it to both the app target and the widget extension. It holdsHaloActivityAttributes(the one attributes type ActivityKit matches on; never copy it),HaloActivityWidget, a ready-made Lock Screen + Dynamic Island UI, and the bridge the Kotlin side finds by itself at run time. - Widget extension — a
WidgetBundlewhose body isHaloActivityWidget(), andNSSupportsLiveActivities = YESin the app'sInfo.plist.
No bridge file to copy, no register call, no export of the Kotlin framework.
Want your own look? Write an ActivityConfiguration(for: HaloActivityAttributes.self) and
read context.state (title, subtitle, progress, timer range, values).
- iOS ends activities after 8 hours. They surface as
LiveActivityState.Expired; the library never restarts them for you. - One slot for the chip.
shortTextreplaces the ticking timer in the status-bar chip and the Dynamic Island compact view. Leave it null to show the timer. - Payload cap. Encoded requests over 3 KB fail with
PayloadTooLarge; ActivityKit's ceiling is about 4 KB. - Restore. Activities still on screen after process death come back in
activitieswith their content, on both platforms. Nothing survives a reboot on Android. - Android
Defaultdismissal keeps the final card as a normal, swipeable notification for up to four hours (best effort; some OEMs ignore timeouts). UseImmediateto remove it now.
| Symptom | Cause |
|---|---|
iOS: start succeeds, nothing renders |
The attributes type is duplicated across targets. Link the Swift package from both targets; do not copy HaloActivityAttributes. |
iOS: Unsupported |
The HaloKMP Swift package is not linked into the app target, or iOS < 16.2. |
| Android: no status-bar chip | Pre-Android 16, canPostPromotedNotifications() false, or shortText and timer both absent. |
sample/composeApp is one shared screen (Start, +5 min, Pause, Finish). Android:
./gradlew :sample:composeApp:installDebug. iOS: cd sample/composeApp/iosApp && xcodegen && open iosApp.xcodeproj.
MIT © Ranbir Singh



