system_live_updates 0.1.0 copy "system_live_updates: ^0.1.0" to clipboard
system_live_updates: ^0.1.0 copied to clipboard

A Flutter plugin for iOS Live Activities and Android live progress notifications with one Dart lifecycle API.

system_live_updates #

A Flutter plugin for long-running, user-visible progress surfaces:

  • iOS Live Activities through ActivityKit.
  • Android 16 live progress notifications through Notification.ProgressStyle.
  • Android fallback ongoing notifications on older supported devices.

The package exposes one Dart lifecycle API for start, update, and end, while still allowing platform-specific options when native behavior differs.

Platform Support #

Platform Surface Minimum version
iOS ActivityKit Live Activities iOS 16.1+
iOS per-activity push update tokens iOS 16.1+
iOS push-to-start capability reporting iOS 17.2+
iOS push-to-start token stream iOS 17.2+
Android Notification.ProgressStyle Android 16 / API 36+
Android promoted Live Updates request Android 16 / API 36+
Android ongoing notification fallback Android 7.0 / API 24+

Use SystemLiveUpdates().capabilities() at runtime. Users can disable notifications or Live Activities in system settings, so version checks alone are not enough.

Quick Start #

Add the package:

flutter pub add system_live_updates

Start a live update after checking permissions:

const liveUpdates = SystemLiveUpdates();

final permissions = await liveUpdates.requestPermissions();
if (!permissions.notificationsEnabled) {
  await liveUpdates.openSettings();
  return;
}

await liveUpdates.start(
  LiveUpdateContent(
    id: 'upload_1',
    title: 'Uploading video',
    subtitle: 'Rendering captions',
    progress: 0.43,
    progressOptions: const LiveUpdateProgressOptions(shortStatus: '43%'),
    android: const AndroidLiveUpdateOptions(channelId: 'uploads'),
    ios: const IosLiveActivityOptions(
      template: IosLiveActivityTemplate.progress,
      symbolName: 'icloud.and.arrow.up',
    ),
  ),
);

For iOS, create a Widget Extension target and install the default template:

dart run system_live_updates:install_ios_template --extension LiveUpdateWidgets
dart run system_live_updates:doctor_ios_template --extension LiveUpdateWidgets

Usage #

const liveUpdates = SystemLiveUpdates();

final permissions = await liveUpdates.requestPermissions();
if (!permissions.notificationsEnabled) {
  await liveUpdates.openSettings();
  return;
}

final capabilities = await liveUpdates.capabilities();
if (!capabilities.isSupported) {
  return;
}

await liveUpdates.start(
  LiveUpdateContent(
    id: 'order_123',
    title: 'Order on the way',
    subtitle: 'Driver is picking up your food',
    progress: 0.35,
    progressOptions: const LiveUpdateProgressOptions(
      shortStatus: '35%',
      segments: [
        LiveUpdateSegment(length: 30, color: 0xFF4F8DFF),
        LiveUpdateSegment(length: 70, color: 0xFF1E7A57),
      ],
      points: [
        LiveUpdatePoint(position: 30, color: 0xFFFFB000),
      ],
    ),
    eta: DateTime.now().add(const Duration(minutes: 18)),
    deepLink: Uri.parse('myapp://orders/123'),
    android: const AndroidLiveUpdateOptions(
      channelId: 'orders',
    ),
    ios: const IosLiveActivityOptions(
      pushType: IosLiveActivityPushType.token,
      template: IosLiveActivityTemplate.journey,
      layout: IosLiveActivityLayout.prominent,
      symbolName: 'takeoutbag.and.cup.and.straw',
      accentColor: 0xFF1E7A57,
      leadingLabel: 'Pickup',
      trailingLabel: '18 min',
      bottomLabel: 'Driver is heading to your address',
      showsEta: true,
      relevanceScore: 80,
      staleAfter: Duration(minutes: 30),
    ),
  ),
);

await liveUpdates.update(
  const LiveUpdateContent(
    id: 'order_123',
    title: 'Order on the way',
    subtitle: 'Driver is nearby',
    progress: 0.82,
  ),
);

await liveUpdates.end(
  'order_123',
  title: 'Delivered',
  subtitle: 'Enjoy your order',
);

Example App #

The included example app is a preset playground. It lets you switch between:

  • progress
  • journey
  • timer
  • status
  • minimal

Each preset uses the same LiveUpdateContent lifecycle while changing titles, labels, metrics, SF Symbols, colors, ETA behavior, and the bundled iOS default template options.

Permissions #

Call requestPermissions() before starting a live update, ideally after the user begins an action that benefits from an ongoing surface.

final permissions = await SystemLiveUpdates().requestPermissions();

if (permissions.notificationsEnabled) {
  // Safe to start/update/end live updates.
} else if (!permissions.canRequestNotifications) {
  // Show your own explanation, then deep-link to app settings.
  await SystemLiveUpdates().openSettings();
}

On Android 13+, this shows the POST_NOTIFICATIONS runtime prompt when possible. On older Android versions, it returns the current notification setting.

On iOS, it requests notification authorization and also reports liveActivitiesEnabled. If Live Activities are disabled in Settings, openSettings() takes the user to the app settings page.

Android Setup #

This package declares:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />

Call SystemLiveUpdates().requestPermissions() from your app before starting a live update. The package declares the manifest permission, but Android still requires a foreground runtime request on Android 13+.

For Android 16, the plugin requests promoted ongoing treatment when AndroidLiveUpdateOptions.requestPromoted is true. The system may still demote or dismiss a notification based on user settings, OEM rules, or eligibility.

iOS Setup #

Add Live Activity support to your app target:

<key>NSSupportsLiveActivities</key>
<true/>

For frequent push updates, also add Apple's frequent updates key in your app target.

For server-driven updates and push-to-start, see doc/ios-push-updates.md.

iOS Live Activities require a Widget Extension to render the Lock Screen and Dynamic Island UI. Flutter packages cannot add this target to the host app automatically, but this package includes default SwiftUI template files at:

ios/templates/SystemLiveUpdateAttributes.swift
ios/templates/SystemLiveUpdatesDefaultWidget.swift

For a step-by-step Xcode walkthrough, see doc/ios-live-activity-extension.md. For fallback behavior, see doc/fallback-notifications.md. For publish preparation, see doc/release-checklist.md.

Default iOS UI #

Use the default template when you want a standard Apple-style Live Activity with title, subtitle, progress, ETA, and Dynamic Island layout:

  1. In Xcode, add a Widget Extension target and select "Include Live Activity".
  2. Add ios/templates/SystemLiveUpdateAttributes.swift and ios/templates/SystemLiveUpdatesDefaultWidget.swift to the Widget Extension target.
  3. Add the default widget to your extension's widget bundle:
import WidgetKit

@main
struct LiveUpdateWidgets: WidgetBundle {
  var body: some Widget {
    if #available(iOS 16.1, *) {
      SystemLiveUpdatesDefaultWidget()
    }
  }
}

The default widget renders the same SystemLiveUpdateAttributes schema that the Dart API starts and updates. The extension owns its compiled copy of that schema so it does not need to link Flutter runtime code.

You can also install the bundled default template into an existing Widget Extension with:

dart run system_live_updates:install_ios_template --extension LiveUpdateWidgets

Run it from your Flutter app root after creating the Widget Extension target in Xcode. The command enables NSSupportsLiveActivities, backs up the extension's Swift entry file, and adds marked imports, WidgetBundle registration, attributes schema, and template source blocks for SystemLiveUpdatesDefaultWidget.

Before or after installing, you can run the read-only doctor:

dart run system_live_updates:doctor_ios_template --extension LiveUpdateWidgets

The doctor checks the local Xcode version, ios/Runner/Info.plist, verified WidgetKit Extension folders, Swift imports, WidgetBundle registration, installed template markers, and the installer backup. It exits with a non-zero status when required setup is missing, so it is safe to use in CI before an iOS release build.

This repository's example app includes a real LiveUpdateWidgets Widget Extension target. To re-run the package's sample compile check:

dart run tool/check_example_ios_widget_extension.dart

That check configures the example Xcode project, runs the iOS template doctor, and builds the example iOS app with flutter build ios --debug --no-codesign.

The installer searches recursively under ios/ for Info.plist files with NSExtensionPointIdentifier set to com.apple.widgetkit-extension. If that identifier is missing, or if the command cannot verify the target safely, it stops before changing files and asks the user to follow the manual path. If your app has more than one Widget Extension, pass either the folder name, an ios/-relative path, or an absolute path:

dart run system_live_updates:install_ios_template --extension Widgets/LiveUpdateWidgets

The --extension option only selects between verified WidgetKit Extension folders. It does not force installation into an unverified folder.

To safely undo the installed default template:

dart run system_live_updates:uninstall_ios_template --extension LiveUpdateWidgets

The uninstall command removes only the marked blocks added by install_ios_template. It requires the installed Swift file to still contain the current system_live_updates block markers and the folder to still be verified as a WidgetKit Extension. If either check fails, it stops without changing files. A full backup restore is available only when explicitly requested with --restore-backup.

The installer supports Xcode 14.1 through Xcode 26.x. Older Xcode versions do not have the expected Live Activity tooling, and newer major versions stop before changing files so the package does not guess a changed Widget Extension layout.

The default widget can be tuned from Flutter with presets and slots in IosLiveActivityOptions:

ios: const IosLiveActivityOptions(
  template: IosLiveActivityTemplate.timer,
  layout: IosLiveActivityLayout.compact,
  symbolName: 'figure.run',
  accentColor: 0xFF007AFF,
  backgroundColor: 0xFFF2F2F7,
  primaryMetric: '12:30',
  secondaryMetric: '3.2 km',
  bottomLabel: 'Zone 2 pace',
  showsSubtitle: true,
  showsProgressBar: true,
  showsProgressRing: false,
  showsProgressLabel: true,
  showsEta: false,
)

Available presets:

Preset Best for
progress uploads, downloads, processing
journey delivery, rides, pickup, travel
timer countdowns, workouts, focus sessions
status queues, bookings, task states
minimal small glanceable states

Optional slots include leadingLabel, trailingLabel, bottomLabel, primaryMetric, and secondaryMetric. These fields only affect the bundled default iOS template. Custom Widget Extensions can ignore them or interpret them differently.

Progress options are shared across platforms:

progressOptions: const LiveUpdateProgressOptions(
  progressMax: 100,
  shortStatus: '35%',
  segments: [
    LiveUpdateSegment(length: 30, color: 0xFF4F8DFF),
    LiveUpdateSegment(length: 70, color: 0xFF1E7A57),
  ],
  points: [
    LiveUpdatePoint(position: 30, color: 0xFFFFB000),
  ],
)

Android maps these fields to Notification.ProgressStyle when available. The bundled iOS default template draws a segmented SwiftUI progress bar and milestone dots from the same fields.

There is intentionally no media preset. Music, podcast, and video playback should use iOS Now Playing controls and Android MediaSession instead of a Live Activity that duplicates system playback UI. This package is for live progress and status surfaces, not audio session management or media remote controls.

Known Limitations #

  • iOS requires the host app to create and ship a Widget Extension target.
  • The iOS default template is flexible, but deeply custom UI still belongs in the app's own Widget Extension.
  • iOS fallback notifications are normal local notification snapshots, not persistent Live Activities.
  • Android promoted Live Updates are requested by the package, but the system can still decline or demote them.
  • Android fallback notifications use the app icon unless the app provides platform-specific notification resources.
  • Media playback surfaces are intentionally out of scope; use iOS Now Playing and Android MediaSession.

Custom iOS UI #

Use custom UI when your app needs branded layouts, buttons, richer Dynamic Island regions, or different Lock Screen composition. Render the same shared attributes type with WidgetKit:

import ActivityKit
import SwiftUI
import WidgetKit

@available(iOS 16.1, *)
struct DeliveryLiveActivity: Widget {
  var body: some WidgetConfiguration {
    ActivityConfiguration(for: SystemLiveUpdateAttributes.self) { context in
      VStack(alignment: .leading) {
        Text(context.state.title)
        if let subtitle = context.state.subtitle {
          Text(subtitle)
        }
      }
    } dynamicIsland: { context in
      DynamicIsland {
        DynamicIslandExpandedRegion(.center) {
          Text(context.state.subtitle ?? context.state.title)
        }
      } compactLeading: {
        Text("Live")
      } compactTrailing: {
        Text(progressText(context.state.progress))
      } minimal: {
        Text(progressText(context.state.progress))
      }
    }
  }

  private func progressText(_ progress: Double?) -> String {
    guard let progress else { return "" }
    return "\(Int(progress * 100))%"
  }
}

Design Guidance #

Use live updates for ongoing, user-initiated, time-sensitive activities with a clear start and end. Good examples are rides, navigation, delivery, workout tracking, timers, and upload progress.

Avoid using live updates for ads, generic promotions, ordinary chat messages, or passive background information.

Avoid using this package as a Now Playing replacement. For media playback, use the platform media APIs so users get the expected Lock Screen controls, Bluetooth/headphone controls, CarPlay behavior, and audio focus handling.

Fallback Behavior #

Android fallback notifications are supported on Android 7.0+ when Android 16 Notification.ProgressStyle is unavailable. The package uses an ongoing notification with title, subtitle, ETA, deep link, and a standard progress bar.

iOS does not have an equivalent ongoing fallback notification surface. If ActivityKit is unavailable or Live Activities are disabled, use capabilities() and requestPermissions() to show in-app status or a normal notification from your app. A normal iOS notification should be treated as an alert, not as a Live Activity replacement.

iOS Push Updates #

Request a per-activity update token when starting a Live Activity:

SystemLiveUpdates().activityPushTokenUpdates().listen((event) {
  // Send event.id and event.token to your backend.
});

await SystemLiveUpdates().start(
  LiveUpdateContent(
    id: 'order_123',
    title: 'Order on the way',
    ios: const IosLiveActivityOptions(
      pushType: IosLiveActivityPushType.token,
    ),
  ),
);

For iOS 17.2+ push-to-start:

SystemLiveUpdates().pushToStartTokenUpdates().listen((event) {
  // Send event.token to your backend.
});
0
likes
160
points
47
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for iOS Live Activities and Android live progress notifications with one Dart lifecycle API.

Homepage
Repository (GitHub)
View/report issues

Topics

#live-activities #live-updates #notifications #activitykit #android16

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on system_live_updates

Packages that implement system_live_updates