Skip to content

Repository files navigation

Halo

One API. Lock Screen, Dynamic Island, and Android Live Updates.

Maven Central Kotlin License

badge-android badge-ios badge-macos badge-jvm badge-wasm

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.

Screenshots

iOS Lock Screen iOS ended Android 16 Live Update Android ended
iOS Lock Screen live activity with a ticking countdown iOS Lock Screen live activity after end with final content Android 16 promoted notification with progress and countdown chronometer Android notification after end with final content

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.

Install

implementation("io.github.androidpoet:halo:0.2.0")          // core API + platform managers
implementation("io.github.androidpoet:halo-compose:0.2.0")  // rememberLiveActivityManager()

Usage

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.

Platforms

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

Android

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.

iOS

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:

  1. Swift package swift/HaloKMP — add it to both the app target and the widget extension. It holds HaloActivityAttributes (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.
  2. Widget extension — a WidgetBundle whose body is HaloActivityWidget(), and NSSupportsLiveActivities = YES in the app's Info.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).

Things to know

  • iOS ends activities after 8 hours. They surface as LiveActivityState.Expired; the library never restarts them for you.
  • One slot for the chip. shortText replaces 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 activities with their content, on both platforms. Nothing survives a reboot on Android.
  • Android Default dismissal keeps the final card as a normal, swipeable notification for up to four hours (best effort; some OEMs ignore timeouts). Use Immediate to remove it now.

Troubleshooting

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

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.

License

MIT © Ranbir Singh

About

Halo: Kotlin Multiplatform Live Activities. ActivityKit on iOS, Android 16 live-update notifications, one API

Topics

Resources

Code of conduct

Stars

13 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages