live_activity_kit

Build iOS Live Activities and Dynamic Island widgets, plus Android Live Updates, in Flutter — entirely from Dart. No Swift, no manual Xcode setup.

pub package platform license

live_activity_kit is a Flutter plugin for iOS Live Activities, the Dynamic Island, and Lock Screen widgets, powered by ActivityKit and WidgetKit, and for Android Live Updates (promoted ongoing notifications on Android 16+) plus custom ongoing notifications on API 26+. You describe the UI with a Flutter-like component DSL in Dart; the package serializes it to a compact JSON layout tree.

Your Dart code  →  JSON layout tree  →  MethodChannel
  iOS:     ActivityKit → SwiftUI renderer
  Android: NotificationManager → RemoteViews / ProgressStyle Live Updates

It is not tied to one use case. The same components build a food delivery tracker, a running tracker, an order status bar, a countdown timer, a meal reminder, a ride tracker, a meditation session, or anything else you can lay out with text, images, stacks and progress.


What Live Activities look like on iOS

Rendered by this package

Dynamic Island rendered by live_activity_kit showing a bicycle SF Symbol and a 12 min ETA

That is the example app running on an iOS 26 simulator. No Swift was written — the whole thing is these six lines of Dart:

await LiveActivity.show(
  id: 'order-1042',
  compactLeading: LA.symbol('bicycle', color: Color(0xFF0A84FF)),
  compactTrailing: LA.text('12 min', size: 13, color: Color(0xFF0A84FF)),
  minimal: LA.symbol('bicycle', color: Color(0xFF0A84FF)),
  lockScreen: LA.column([
    LA.text('Out for delivery', size: 20, weight: FontWeight.bold),
    LA.progress(0.7, tint: Color(0xFF0A84FF)),
  ], spacing: 6),
);

The four presentations

A Live Activity is one activity with four presentations. iOS picks which to show based on where the user is looking — and this package lets you give each one its own component tree. The images below illustrate the iOS presentations themselves.

Dynamic Island — minimal, compact, expanded

iOS Dynamic Island showing minimal, compact and expanded Live Activity presentations

The same activity collapses to a couple of glyphs beside the sensor housing, and expands on long-press into a four-region layout. In Dart: minimal:, compactLeading: / compactTrailing:, and the four expanded*: parameters.

Lock Screen — the full banner

iOS Lock Screen Live Activities showing meal timing, fasting progress and interactive buttons

The roomiest presentation, and the one most apps spend their design effort on. In Dart: lockScreen:, built from LA.column / LA.row / LA.progress / LA.countdown.

Order & delivery tracking, updating over time

Food delivery Live Activity progressing through preparing, out for delivery and arriving states on the iOS Lock Screen

One activity, pushed through several states as the order progresses — see the delivery recipe for the exact code.

Workouts and fitness

Workout Live Activity in the iOS Dynamic Island with a timer and progress ring

Rings, timers and stat rows — LA.circularProgress, LA.stopwatch and LA.metric. See the workout recipe.

About these images: they are Apple and third-party app marketing materials, included to illustrate the iOS Live Activity presentations this package targets. They are not screenshots of the bundled example app, and are not covered by this package's MIT licence.


Contents


Why this package

Adding a Live Activity to a Flutter app normally means leaving Flutter entirely. You have to create a Widget Extension target by hand, write SwiftUI views, define ActivityAttributes, configure an App Group, add entitlements, wire a MethodChannel, and then keep two separate UIs in sync forever.

Apple does not allow a Flutter engine to render inside a Live Activity — the UI must be SwiftUI, in a separate extension process. That constraint is not going away, so this package absorbs it instead of fighting it:

Doing it by hand With live_activity_kit
Create the Widget Extension in Xcode dart run live_activity_kit:setup
Write SwiftUI views per feature Write LA.column([...]) in Dart
Define ActivityAttributes / ContentState Generated, generic, already done
Configure App Group + entitlements Automated
Hand-roll a MethodChannel Built in
Maintain Swift + Dart UI in parallel One UI, in Dart

You still get real SwiftUI rendering and real ActivityKit behaviour — you just never open the Swift files.


Quick start (5 minutes)

1. Add the dependency

flutter pub add live_activity_kit

2. Generate the widget extension

dart run live_activity_kit:setup

3. Enable the App Group in Xcode (once — it needs a signed-in Apple ID)

open ios/Runner.xcworkspace
Runner target                → Signing & Capabilities → + App Groups → tick your group
LiveActivityKitWidget target → Signing & Capabilities → + App Groups → tick the same group

4. Start an activity from Dart

import 'package:live_activity_kit/live_activity_kit.dart';

await LiveActivity.show(
  id: 'meal',

  // Dynamic Island, collapsed
  compact: LA.text('🍱 1:00'),

  // Dynamic Island, expanded
  expanded: LA.column([
    LA.text('Next meal', weight: FontWeight.bold),
    LA.text('Lunch — 1:00 PM'),
    LA.progress(0.65),
  ]),

  // Lock Screen banner
  lockScreen: LA.column([
    LA.row([
      LA.text('Next meal', weight: FontWeight.bold),
      LA.spacer(),
      LA.text('LIVE', size: 11, color: Colors.red),
    ]),
    LA.text('Lunch — 1:00 PM', size: 20),
    LA.progress(0.65),
    LA.row([
      LA.text('In 42 min'),
      LA.spacer(),
      LA.text('420 kcal'),
    ]),
  ]),
);

5. Update it and end it

await LiveActivity.update(id: 'meal', lockScreen: LA.text('Dinner — 7:30 PM', size: 20));
await LiveActivity.end(id: 'meal');

That's the whole API surface for most apps: show, update, end.


Installation & setup

Requirements

iOS 16.1+ (Lock Screen), iPhone 14 Pro and later for the Dynamic Island
Android API 26+ ongoing notifications; Android 16+ Live Updates (status bar chip / lock screen) when the system promotes them
Xcode 15 or newer (iOS)
Flutter 3.27 or newer
Device Real device required on iOS 16; the iOS 17+ simulator works. Android emulator is fine.

No Gradle changes needed. The plugin builds against compileSdk 35 with minSdk 21, so flutter pub add live_activity_kit is the whole install — you do not have to raise your app's minSdk or compileSdk. Below API 26 every call throws LiveActivityException('unsupported'). Android 16 Live Updates are invoked reflectively, so they light up on Android 16+ devices without pulling every consumer onto compileSdk 36.

Web and desktop remain safe no-ops: LiveActivity.support() reports isSupported: false and calls throw LiveActivityException('unsupported', …), so one codebase ships everywhere.

On Android, show() asks for POST_NOTIFICATIONS (API 33+) and posts an ongoing notification. LA.progress becomes a progress bar, and on Android 16+ a Notification.ProgressStyle Live Update when the OS allows promotion. Dynamic Island regions are ignored; the lock-screen tree is what is drawn. APNs push-to-start is iOS only.

LA.countdown / LA.stopwatch map to a system Chronometer that ticks without Dart updates — but only when the same region has no LA.progress. A layout with progress is rendered by the system's ProgressStyle template on Android 16+, which replaces the plugin's custom view, so the countdown and the progress label are not drawn. Put the countdown in a progress-free region if it must tick on Android. A LACountdownStyle.time countdown is a static wall-clock label and never drives the Chronometer.

Known Android limitations

  • Activity state does not survive the app process. Running activities are tracked in memory. If Android kills the process, the ongoing notification stays on screen but activities() returns nothing and update / end throw not_found, so the leftover notification cannot be addressed. iOS restores activities across launches; Android does not. Track long-running work with your own persistence.
  • Deep links are dropped on a cold tap. Tapping an activity launches the app, but if the process was not already running the URL is not delivered to LiveActivity.deepLinks.
  • enablePush is ignored. There is no APNs equivalent; drive Android updates from your own push handler by calling update().
  • Notification ids are 16-bit. Two activity ids whose hashes collide will overwrite each other's notification.

What setup does for you

dart run live_activity_kit:setup
  1. Creates ios/LiveActivityKitWidget/ — the widget extension plus the SwiftUI renderer.
  2. Derives an App Group (group.<your.bundle.id>.liveactivity) and writes it into Runner.entitlements, the extension entitlements, and both Info.plist files.
  3. Adds NSSupportsLiveActivities to ios/Runner/Info.plist.
  4. Registers the extension target in Runner.xcodeproj, embeds it in your app, and sets its bundle identifier, entitlements, deployment target and Swift version.

Re-running it is safe: generated renderer files are re-synced, and files you edited are left alone unless you pass --force.

dart run live_activity_kit:setup \
  --app-group=group.com.acme.app.liveactivity \  # custom App Group id
  --target-name=AcmeWidget \                     # custom extension target name
  --deployment-target=16.1 \
  --frequent-updates \                           # NSSupportsLiveActivitiesFrequentUpdates
  --force \                                      # overwrite edited widget sources
  --no-xcode                                     # write files, don't touch the project

The three steps a script cannot do

  1. Add the App Group capability in Xcode for both targets. App Groups are a signing capability — creating one requires an authenticated Apple ID session that a CLI does not have.
  2. Set a development team on both targets, if Runner doesn't have one.
  3. Build once from Xcode, so provisioning profiles are materialised for the new target. After that, flutter run handles everything.

If the Xcode step fails (usually because the xcodeproj Ruby gem is missing — it ships with CocoaPods), the command prints exact manual instructions and exits non-zero.


Recipes

Copy-paste starting points for the most common Live Activity use cases.

Food delivery / order tracking

await LiveActivity.show(
  id: 'order-1042',
  theme: LATheme(tint: Colors.blue, foreground: Colors.white),
  deepLink: 'myapp://order/1042',

  compactLeading: LA.symbol('bicycle'),
  compactTrailing: LA.countdown(eta, style: LACountdownStyle.relative),
  minimal: LA.symbol('bicycle'),

  lockScreen: LA.column([
    LA.row([
      LA.symbol('bicycle', size: 14, color: Colors.blue),
      LA.text('Order #1042', size: 13, weight: FontWeight.w600),
      LA.spacer(),
      LA.countdown(eta, style: LACountdownStyle.relative, prefix: 'arrives'),
    ], spacing: 6),
    LA.text('Out for delivery', size: 20, weight: FontWeight.bold),
    LA.progress(0.7, tint: Colors.blue, label: '3/4'),
  ], spacing: 6),

  enablePush: true,      // let your server drive it — see "Push updates"
  relevanceScore: 80,
);

Workout / running tracker

await LiveActivity.show(
  id: 'run',
  compactLeading: LA.symbol('figure.run', color: Colors.orange),
  compactTrailing: LA.stopwatch(startedAt),        // ticks by itself, no updates

  expandedLeading: LA.metric('4.21', unit: 'km', label: 'distance'),
  expandedTrailing: LA.metric('5:12', unit: '/km', label: 'pace', align: LAAlign.end),
  expandedCenter: LA.stopwatch(startedAt, size: 22, weight: FontWeight.bold),
  expandedBottom: LA.progress(0.84, tint: Colors.orange),

  lockScreen: LA.row([
    LA.circularProgress(0.84, size: 54, lineWidth: 6, tint: Colors.orange,
        center: LA.text('84', size: 15, weight: FontWeight.bold)),
    LA.column([
      LA.stopwatch(startedAt, size: 26, weight: FontWeight.bold),
      LA.row([
        LA.metric('4.21', unit: 'km', label: 'distance'),
        LA.metric('5:12', unit: '/km', label: 'pace'),
        LA.metric('261', unit: 'kcal', label: 'burned'),
      ], distribution: LADistribution.spaceBetween),
    ], spacing: 4),
  ], spacing: 14),

  staleAfter: const Duration(minutes: 1),   // dim it if GPS stops feeding us
);

Countdown timer / meeting reminder

The cheapest possible activity — started once, never updated:

await LiveActivity.show(
  id: 'meeting',
  compactLeading: LA.symbol('video.fill'),
  compactTrailing: LA.countdown(startsAt),
  lockScreen: LA.column([
    LA.text('Design review', size: 14, opacity: 0.8),
    LA.countdown(startsAt, size: 34, weight: FontWeight.bold),
  ], spacing: 4),
);
// No update() calls needed — the system redraws the clock.

Order status with a final frame

await LiveActivity.update(
  id: 'order-1042',
  lockScreen: LA.text('Arriving now', size: 20, weight: FontWeight.bold),
  alert: const LiveActivityAlert(title: 'Almost there', body: 'Your order is nearby'),
);

await LiveActivity.end(
  id: 'order-1042',
  lockScreen: LA.row([
    LA.symbol('checkmark.circle.fill', size: 22, color: Colors.green),
    LA.text('Delivered', size: 17, weight: FontWeight.bold),
  ], spacing: 10),
  policy: LiveActivityEndPolicy.after(DateTime.now().add(const Duration(minutes: 5))),
);

The example/ app contains all four of these as complete, runnable demos.


Components

Each component is an LA.* factory returning an immutable LANode. Arbitrary Flutter widgets are not supported (see Limitations) — this fixed set is what SwiftUI can render inside a Live Activity.

Component What it renders
LA.text Text, emoji, any Unicode SwiftUI can draw
LA.symbol An SF Symbol — zero payload cost, always instant
LA.asset A Flutter asset (auto-copied into the App Group)
LA.networkImage A remote image (downloaded and cached by the app, not the extension)
LA.row Horizontal stack (SwiftUI HStack)
LA.column Vertical stack (SwiftUI VStack)
LA.progress Linear progress bar, optional caption
LA.circularProgress Progress ring, optional centred node
LA.metric Value + unit + label + icon — for stat rows
LA.countdown / LA.stopwatch Self-updating timer — needs no updates at all
LA.spacer Flexible space
LA.divider Hairline rule
LA.padding Insets around a child
LA.container Background / gradient / corner radius / border / fixed size

Styling

The DSL takes the familiar dart:ui types, so it reads like Flutter code:

LA.text(
  'Lunch',
  size: 20,
  weight: FontWeight.bold,        // Flutter FontWeight
  color: Colors.green,            // Flutter Color
  align: TextAlign.center,        // Flutter TextAlign
  maxLines: 2,
  opacity: 0.8,
  italic: true,
  monospacedDigit: true,          // stops digits jittering as they change
  uppercase: true,
);

LA.row(
  [...],
  spacing: 8,
  align: LAAlign.center,                      // cross axis
  distribution: LADistribution.spaceBetween,  // main axis
);

LA.container(
  child: LA.text('Badge'),
  gradient: const [Color(0xFF06214A), Color(0xFF0B0B0F)],
  cornerRadius: 12,
  padding: const LAInsets.symmetric(horizontal: 8, vertical: 4),
);

Timers deserve their own paragraph

LA.countdown maps to SwiftUI's Text(date, style:), which the system redraws. A counting clock therefore needs zero updates — it keeps ticking while your app is suspended or even terminated:

LA.countdown(meetingStart)                             // 04:32, ticking down
LA.countdown(eta, style: LACountdownStyle.relative)    // "in 12 minutes"
LA.countdown(eta, style: LACountdownStyle.time)        // "1:04 PM"
LA.stopwatch(startedAt)                                // counts up

Pushing a new LA.text('04:32') every second instead is the single most common way apps burn through iOS's Live Activity update budget and end up frozen. Use the countdown.


Regions: Lock Screen & Dynamic Island

iOS shows your activity in several places, each with its own space. Every region takes its own component tree.

COLLAPSED DYNAMIC ISLAND            MINIMAL (two activities competing)
┌───────────────────────────┐       ┌────┐        ┌────┐
│ ●compactLeading   compact │       │ ●  │  ····  │ ●  │
│                 Trailing● │       └────┘        └────┘
└───────────────────────────┘       one glyph of space

EXPANDED DYNAMIC ISLAND             LOCK SCREEN / BANNER / STANDBY
┌─────────────────────────────┐     ┌─────────────────────────────┐
│ expandedLeading   Trailing  │     │                             │
│       expandedCenter        │     │        lockScreen           │
│       expandedBottom        │     │                             │
└─────────────────────────────┘     └─────────────────────────────┘
Parameter Where it appears
lockScreen Lock Screen, notification banner, StandBy
compactLeading Collapsed island, left of the sensor housing
compactTrailing Collapsed island, right of it
compact Shorthand for compactTrailing
minimal When another activity competes for the island — room for one glyph
expandedLeading / expandedTrailing / expandedCenter / expandedBottom Expanded island
expanded Shorthand for expandedBottom, ignored if you set another expanded region

Fallbacks: compactTrailing ← compact, and minimal ← compactTrailing ← compact. The lock-screen tree is deliberately never reused for the island — it is almost always too tall and would be clipped.

Theming

theme: LATheme(
  background: const Color(0xFF0B1F14),                            // Lock Screen tint
  backgroundGradient: const [Color(0xFF06214A), Color(0xFF0B0B0F)],
  tint: Colors.green,          // Dynamic Island keyline glow
  foreground: Colors.white,    // default text colour for the whole tree
)

Updating and ending activities

Update

Regions you omit keep their previous content, so a partial update is normal and cheap:

await LiveActivity.update(
  id: 'meal',
  lockScreen: LA.text('Dinner'),        // compact/expanded regions are preserved
);

await LiveActivity.update(
  id: 'meal',
  lockScreen: LA.text('Dinner'),
  merge: false,                         // replace the layout outright
);

Optional extras:

Parameter Effect
alert: Shows a banner and wakes the screen. iOS rate-limits these — use for real status changes, not every tick
staleAfter: iOS dims the activity once the content is this old
relevanceScore: Orders competing activities in the Dynamic Island

End

await LiveActivity.end(id: 'run');                                        // default window
await LiveActivity.end(id: 'run', policy: const LiveActivityEndPolicy.immediate());
await LiveActivity.end(
  id: 'run',
  lockScreen: LA.text('Workout complete'),                                // final frame
  policy: LiveActivityEndPolicy.after(DateTime.now().add(const Duration(minutes: 5))),
);
await LiveActivity.endAll(immediate: true);                               // e.g. on logout

With the default policy iOS keeps that final frame on the Lock Screen for up to four hours — usually exactly what you want for "Delivered" or "Workout complete".

Observing

LiveActivity.states.listen((change) => print('${change.id} → ${change.state.name}'));
LiveActivity.deepLinks.listen(router.go);                 // taps on the activity

final running = await LiveActivity.activities();          // survives app restarts
final isLive  = await LiveActivity.isRunning('meal');
final support = await LiveActivity.support();             // canStart, supportsDynamicIsland…

Multiple activities run simultaneously — just give each one its own id.


Push updates with APNs

If the data lives on a server (delivery, ride, flight, sports score), update the activity from the server. Your app cannot reliably update a Live Activity while backgrounded, and cannot update it at all once terminated.

await LiveActivity.show(id: 'order-1042', enablePush: true, /* … */);

LiveActivity.pushTokens
    .where((t) => t.id == 'order-1042')
    .listen((t) => api.registerActivityToken(t.id, t.token));   // tokens rotate — resend

// iOS 17.2+: start an activity from the server, app never launched
final startToken = await LiveActivity.pushToStartToken();

The APNs payload, topic format, content-state shape, push-to-start, and the rules about revision and timestamp are documented in full — with working JSON — in doc/background_and_push.md.

Rule of thumb: anything driven by time → LA.countdown. Anything driven by a server → APNs. Anything driven by the user while the app is open → LiveActivity.update.


App Group storage

The app and the widget extension are separate processes. The layout travels inside the 4 KiB ActivityKit content state; anything bigger goes through the shared App Group:

await LiveActivityStore.write('order', {'id': 1042, 'items': items});
final order = await LiveActivityStore.readMap('order');

LiveActivityStore.changes.listen((key) => print('$key changed'));
LiveActivityStore.watch('order').listen(print);

Changes are broadcast with a Darwin notification — the only cross-process signal available to an app extension. From Swift, use LiveActivityAppGroup in either target.


Performance

Live Activities run in a memory-capped extension and redraw often, so the package is built around that:

  • Small payloads. Nulls are never serialized, colours are #RRGGBB, insets are 4-element arrays. A realistic four-region layout is 500–900 bytes against ActivityKit's 4096-byte ceiling — and LiveActivity throws payload_too_large in Dart, naming the activity, before iOS can reject it silently.
  • Fast decode. Swift decodes with JSONSerialization into dictionaries rather than Codable; for a heterogeneous, deeply optional tree redrawn every second that is measurably cheaper.
  • Low memory. Images are downsampled to their display size before decoding, and remote images are fetched by the app, never by the extension (which is jetsam-killed long before your app would be).
  • Frequent updates. Put anything time-based in LA.countdown and push only values that genuinely change. That is what makes a 1–2 second workout tracker viable.

Limitations

Read this section before you design your UI. These are platform constraints, not a backlog:

  • Arbitrary Flutter widgets. Apple does not permit a Flutter engine inside a widget extension. No package can do this — the UI must be SwiftUI.
  • WebView, video, Canvas, CustomPainter.
  • Custom gestures. A Live Activity supports a tap (deep link) and, on iOS 17+, App Intent buttons. No drags, no scrolling, no swipes.
  • Complex animations. iOS animates content-state transitions itself; you cannot drive keyframes, and elaborate animations are dropped.
  • Custom fonts. System fonts only, unless you bundle fonts into the extension by hand.
  • Pixel-identical Android UI. Android has no Dynamic Island and no SwiftUI widget extension. The same Dart tree is flattened into an ongoing notification (RemoteViews +, on Android 16, Live Updates). Nested stacks are summarized; SF Symbols become the app's small icon, not Apple glyphs.
  • Web / desktop. No equivalent OS feature exists.
  • ⚠️ Budgets: 4 KiB per content state, a handful of concurrent activities, a limited update rate, an 8-hour activity lifetime and 12-hour Lock Screen lifetime.

Troubleshooting

Nothing appears on Android. Grant notification permission. LiveActivity.support() must report canStart: true. Check the shade / lock screen for an ongoing notification. Live Updates (status-bar chip) need Android 16+ and a layout that includes LA.progress.

Nothing appears on screen. Check in order: LiveActivity.support() returns canStart: true; both targets have the same App Group ticked in Signing & Capabilities; NSSupportsLiveActivities is in ios/Runner/Info.plist; the extension is embedded in the app; you passed at least one region to show.

LiveActivityException(disabled). The user turned Live Activities off (iOS) or notifications off (Android) for this app. Re-check with LiveActivity.support(refresh: true).

LiveActivityException(payload_too_large). Your tree serialized past 4 KiB. Shorten text, drop network images, or move per-second values into LA.countdown.

LiveActivityException(app_group_missing). The App Group capability isn't attached, or the two targets disagree. Re-run dart run live_activity_kit:setup and re-check Xcode.

Updates stop after a while. You are over iOS's update budget, or the activity hit the 8-hour cap. Move time-based values to LA.countdown; consider --frequent-updates.

The UI freezes at an old value. Your app was suspended. Set staleAfter so iOS dims it, and move to APNs push updates.

The widget extension won't build. Open ios/Runner.xcworkspace (not .xcodeproj) and confirm the LiveActivityKitWidget target has a development team and a deployment target of 16.1 or higher.

The plugin "live_activity_kit" requires a higher minimum iOS deployment version. Your Podfile still targets iOS 12. setup raises it to 13.0 automatically — re-run dart run live_activity_kit:setup, or set platform :ios, '13.0' in ios/Podfile yourself and run pod install.

Cycle inside Runner; building could produce unreliable results. Xcode appended "Embed Foundation Extensions" after Flutter's "Thin Binary" phase, and the two form a cycle. setup reorders them; if you added the target by hand, drag "Embed Foundation Extensions" above "Thin Binary" in Runner → Build Phases.

Failed to create app extension placeholder on install. The extension's Info.plist has no CFBundleVersion / CFBundleShortVersionString, because $(FLUTTER_BUILD_NUMBER) did not resolve. setup points the target at Flutter/Generated.xcconfig to fix this — re-run it after a flutter pub get.

ld: framework 'Flutter' not found. Not a package problem: your Flutter SDK's Flutter.xcframework is missing a slice binary. Run flutter precache --ios --force.


FAQ

Do I have to write any Swift? No. setup generates the extension and the renderer. Swift is only involved if you choose to add your own components.

Can I use my existing Flutter widgets in the Live Activity? No — and neither can any other package. The Live Activity UI is rendered by a system process outside your app, where only WidgetKit/SwiftUI views are allowed.

Does it work in the simulator? Yes on iOS 17+. On iOS 16 you need a real device.

Can I add my own component? Yes: add a LANode subclass in Dart, a case in LANode.parse, and a view in LANodeRenderer.swift, then re-run setup to sync the extension's copy.

Can I run several activities at once? Yes — one per id. iOS doesn't document a hard limit; show throws too_many_activities when the system refuses.

Can I start a Live Activity from a push notification? On iOS 17.2+, yes — LiveActivity.pushToStartToken().

Does this support Android? Yes. The same LiveActivity.show / update / end API posts an ongoing notification on API 26+. On Android 16+ it requests Live Updates (setRequestPromotedOngoing) when the layout includes progress. There is no Dynamic Island. LiveActivity.support() reports supportsLiveUpdates when the system will promote the notification. Read Known Android limitations before you rely on it for long-running work.

How do I test without a device? Set LiveActivityPlatform.instance = YourFakePlatform(). The platform interface is public so widget and unit tests never need hardware.


Example app

example/ contains four complete demos, each with start → update → end:

Demo What it demonstrates
Meal reminder Ambient info; started once, LA.countdown does the rest
Running tracker 2-second updates with a system stopwatch and progress ring
Delivery tracking APNs push tokens, per-stage alerts, deep links, gradients
Meeting countdown Fire-and-forget; zero updates after start
cd example
dart run live_activity_kit:setup
flutter run

Testing

flutter test                                   # DSL, JSON schema, layout, facade
cd example && flutter test integration_test    # on-device ActivityKit tests

Android layout flattening tests live in android/src/test/ (LayoutFlattenerTest).

Swift decoder and renderer tests live in templates/ios/Tests/LANodeTests.swift — add the file to a unit-test target that compiles the generated widget sources and run ⌘U. The Dart and Swift suites assert against the same JSON payloads, which is what keeps both halves of the schema honest.


Contributing

Issues and pull requests are welcome at github.com/NIAnup/live_activity_kit.

License

MIT — see LICENSE.


Keywords: flutter live activities, ios dynamic island flutter, activitykit flutter plugin, flutter lock screen widget, widgetkit flutter, flutter ios widget extension, live activity package, flutter delivery tracking ui, flutter workout live activity, dynamic island package, flutter apns live activity push, android live updates flutter, android ongoing notification flutter.

Libraries

live_activity_kit
A universal, component-based framework for iOS Live Activities / Dynamic Island and Android Live Updates (ongoing notifications), driven from Dart.