-
Notifications
You must be signed in to change notification settings - Fork 100
Description
I’m seeing a puzzling behavior: the Live Activity starts and renders on the Lock Screen/Dynamic Island, and Flutter logs report that updateActivity(...) calls succeed with the full payload. However, the widget UI never reflects those updates—it only shows the default values defined in Swift.
Environment
-
Flutter: stable (iOS, physical device)
-
Plugin:
live_activities: ^2.4.2 -
Device iOS: 26.0.1
-
Runner (app) min iOS: 13
-
Extension min iOS: 18.2 (Live Activities supported; also annotated with
@available(iOSApplicationExtension 16.1, *)) -
Xcode: 26.0.1
-
Entitlements:
- App and Extension: Live Activities + App Groups
- App Group:
group.com.example.app(redacted)
-
Embedding: Extension embedded via Build Phases → Embed Foundation Extensions → Destination: Plugins and Foundation Extensions
-
Runner Target Dependencies includes the extension; Skip Install = Yes on extension
Symptom
createActivity(...)returns an ID and Live Activity renders.- Repeated
updateActivity(...)calls log “updated successfully” with full state. - UI remains on Swift defaults (never reflects Flutter-sent values).
Minimal Repro (Dart)
await live.init(appGroupId: 'group.com.example.app', urlScheme: 'vns');
final id = await live.createActivity(
'LiveActivitiesAppAttributes', // also tried '<EXTENSION_TARGET_NAME>.LiveActivitiesAppAttributes'
{
'name': 'Stress Session',
'state': 'active',
'endTime': '16:33',
'statusMessage': 'Session started',
'intensity': 1,
'isPaused': false,
},
);
await live.updateActivity(id, {
'name': 'Stress Session',
'state': 'active',
'endTime': '16:33',
'statusMessage': 'Intensity level 2',
'intensity': 2,
'isPaused': false,
});Widget (Swift, simplified)
I tried both paths: reading directly from
context.state.*and reading from App GroupUserDefaultsusing a prefixed key. Neither path reflects Flutter updates; the UI stays on defaults.
import ActivityKit
import WidgetKit
import SwiftUI
import AppIntents
private func appGroupId() -> String { "group.com.example.app" }
let sharedDefaults = UserDefaults(suiteName: appGroupId())!
struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
public typealias LiveDeliveryData = ContentState
public struct ContentState: Codable, Hashable {
var name: String?
var emoji: String?
var state: String?
var endTime: String?
var statusMessage: String?
var intensity: Int?
var isPaused: Bool?
}
var id = UUID()
}
@available(iOSApplicationExtension 16.1, *)
struct SessionLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in
// Path A: direct ContentState (tried this)
// let sessionName = context.state.name ?? "Default"
// Path B: App Group defaults (tried this as a sanity test)
let sessionName = sharedDefaults.string(forKey: context.attributes.prefixedKey("name")) ??
<img width="603" height="208" alt="Image" src="https://github.com/user-attachments/assets/b4c7b1cc-f1ba-42ee-a431-a30b1e82496e" />
let endTime = sharedDefaults.string(forKey: context.attributes.prefixedKey("endTime")) ?? "00:00"
let statusMsg = sharedDefaults.string(forKey: context.attributes.prefixedKey("statusMessage")) ?? "active"
let intensity = sharedDefaults.integer(forKey: context.attributes.prefixedKey("intensity"))
let isPaused = sharedDefaults.bool(forKey: context.attributes.prefixedKey("isPaused"))
VStack {
Text(sessionName)
Text(endTime)
Text(statusMsg)
Text("\(intensity)")
Text(isPaused ? "paused" : "active")
}
}
}
}
extension LiveActivitiesAppAttributes {
func prefixedKey(_ key: String) -> String { "\(id)_\(key)" }
}Logs (trimmed)
[log] Live Activities service initialized successfully
[log] Creating new Live Activity: {name: Stress Session, emoji: ⚡, state: active, endTime: 16:33, statusMessage: Session started, intensity: 1, isPaused: false}
[log] Live Activity created successfully with ID: 148D781F-E307-446B-AEC4-DA2D80A9B012
[log] Sending Live Activity update with full state: {name: Stress Session, emoji: ⚡, state: active, endTime: 16:33, statusMessage: Intensity level 1, intensity: 1, isPaused: false}
[log] Live Activity updated successfully
[log] Sending Live Activity update with full state: {... intensity: 2, ...}
[log] Live Activity updated successfully
What I’ve already tried
- ✅ Runner → Target Dependencies includes extension
- ✅ Extension Skip Install = Yes
- ✅ Entitlements on both targets: Live Activities, App Groups (
group.com.example.app) - ✅ Build once in Xcode; physical device
- ✅ Tried passing module-qualified type:
'<EXTENSION_TARGET_NAME>.LiveActivitiesAppAttributes' - ✅ Renamed
state→sessionStatein both keys and Swift struct (to dodge any reserved-name quirks) - ✅ iOS 17-only
Button(intent:)controls are availability-gated (not relevant to this decode issue) - ✅ Also tested reading state via UserDefaults (App Group) with prefixed key—UI still shows defaults
Redaction
- Bundle IDs, group IDs, and product names are replaced with placeholders like
group.com.example.appand<EXTENSION_TARGET_NAME>.
Any pointers appreciated
If there’s a known requirement to compile the attributes in both targets (app + extension) when using Flutter, or another gotcha that produces this “defaults only” behavior despite successful updates, I’d love guidance.