Here’s the first thing to know about Flutter Live Activities: none of the code that draws them can be Dart. A Live Activity is SwiftUI running inside a widget extension, rendered by the system on the Lock Screen and in the Dynamic Island, and your Flutter engine isn’t there. What Flutter can do is drive it. Your Dart code decides when an activity starts, what it shows and when it ends, and a thin Swift layer does the rest.
We built this for Crumb Count, our nutrition app, which uses platform channels into Swift for Live Activities, the Dynamic Island and lock-screen widgets. Here’s the setup, and the limits Apple documents that will shape your design.
The moving parts of Flutter Live Activities
You need four things in the iOS project that flutter create gave you:
- A widget extension target. Add it in Xcode (File, New, Target, Widget Extension, with “Include Live Activity” ticked). This is where the SwiftUI layouts live.
- An
ActivityAttributestype shared by both targets. The app target creates activities and the extension renders them, so the same Swift file needs target membership in both. - The
NSSupportsLiveActivitieskey set toYESin the app’s Info.plist. - A
MethodChannelbetween Dart and the app target, so Flutter can say start, update and end.
The attributes type splits your data in two. Static fields (set once, at start) sit on the struct itself. Anything that changes lives in the nested ContentState:
import ActivityKit
struct TimerAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var endsAt: Date
var label: String
}
var title: String
}
On the Swift side of the channel, starting an activity is one call. The pushType: .token argument is only needed if your server will update the activity over APNs:
let state = TimerAttributes.ContentState(endsAt: endsAt, label: label)
let activity = try Activity.request(
attributes: TimerAttributes(title: title),
content: .init(state: state, staleDate: nil),
pushType: .token)
And from Dart, it’s an ordinary channel call:
const _live = MethodChannel('app/live_activity');
await _live.invokeMethod('start', {
'title': 'Workout',
'label': 'Interval 3 of 8',
'endsAt': endsAt.millisecondsSinceEpoch,
});
Keep the Dart API boring: start, update, end, plus a stream of events coming back. Resist the urge to mirror every ActivityKit option across the channel. You’ll use three of them.
The limits that decide your design
Apple’s ActivityKit documentation sets hard limits, and they’re worth reading before anyone draws a mockup:
- Eight hours. A Live Activity can be active for up to eight hours unless the app or the user ends it. After that the system ends it and removes it from the Dynamic Island.
- Four more hours on the Lock Screen. An ended activity can stay on the Lock Screen for up to four hours (or until the user clears it), so twelve hours in total at most.
- 4 KB. Static and dynamic data together, including what arrives in push updates, can’t exceed 4 KB. No images in the payload. Bundle images in the extension and refer to them by name.
- No network, no location. Each Live Activity runs in its own sandbox and, unlike a widget, can’t fetch anything or receive location updates. New data arrives only from your app through ActivityKit, or from your server as an ActivityKit push notification.
That last point catches Flutter teams out. If you want a ticking clock, don’t send an update every second. SwiftUI’s Text(timerInterval:countsDown:) renders a live countdown from a date range with zero updates from you. Send the end time once, and the system animates it.
Dynamic Island layouts
The ActivityConfiguration in your extension describes a Lock Screen view plus the Dynamic Island in three presentations: compact (leading and trailing pieces either side of the camera), minimal (when two activities share the island) and expanded (when the user long-presses). Design the compact view first. It’s tiny and it’s what people see most. One number and one glyph is usually the right amount.
Updating from the server with ActivityKit push
When the app is suspended, it can’t call update, so anything driven by server events (an order status, a live score) needs ActivityKit push notifications. The flow:
- After starting the activity, read
activity.pushTokenUpdatesin Swift. This is an async sequence, and the token can change, so keep listening. - Forward each token to Dart over an
EventChannel, and from Dart to your backend along with the activity’s ID. - Your server sends to APNs with
apns-push-type: liveactivityand the topic<bundle id>.push-type.liveactivity. The payload’sapsobject carries anevent(updateorend), atimestampand acontent-statethat must decode into yourContentState.
Priority matters. Priority 10 is delivered immediately but counts against a budget, and if you burn through it the system throttles you. Priority 5 doesn’t count against that budget. Use 10 for the update a user is actually waiting for, and 5 for everything else. If your app legitimately needs frequent updates, add NSSupportsLiveActivitiesFrequentUpdates to Info.plist and check ActivityAuthorizationInfo().frequentPushesEnabled at runtime, because the user can turn it off.
From iOS 17.2 there’s also push-to-start: Activity.pushToStartTokenUpdates gives your server a token that can start an activity remotely with an event of start. Treat it as a nice addition, not the base design, since older iOS versions in your user base won’t have it.
Gotchas we’d warn you about
- Users can switch Live Activities off per app. Check
ActivityAuthorizationInfo().areActivitiesEnabledand have the Flutter UI degrade quietly when it’s false. - Your
ContentStateis a contract between app, extension and server. Rename a field and old pushes stop decoding. Version it like an API. - On launch, list
Activity<TimerAttributes>.activitiesand reconcile with Dart state. After a crash or an update, an activity from the last session may still be on screen. - Android has no Live Activities. The closest match is an ongoing notification, often attached to a foreground service. Put that behind the same Dart API so feature code doesn’t care which platform it’s on.
None of this is Flutter-specific beyond the channel. A React Native app does exactly the same thing with a native module in place of the MethodChannel.
If you’d like Live Activities, widgets or other native surfaces added to a Flutter app without rewriting it, that’s ordinary work for us at orithLabs. See our mobile development services, or how it came together in Crumb Count.