Flutter 即時動態(Live Activity)&動態島(Dynamic Island)實作筆記


#Flutter#Live Activity#Dynamic Island
即時動態(Live Activities)是 iOS 16.1 新增的功能,讓 APP 可以在鎖定畫面、通知中心或動態島顯示即時更新的資訊。使用者可以不必打開 APP 就能輕鬆查看一些即時資訊,提升便利性和互動體驗。


Flutter package


使用 https://pub.dev/packages/live_activities

需要寫一些 Swift,這篇文章主要介紹我使用時遇到的問題跟要注意的事項。


1. 建立 Widget Extension 時不要勾選 Include Configuration App Intent


2. swift 的 LiveActivitiesAppAttributes 命名不可修改

Inside your extension ExtensionNameLiveActivity.swift file, you need to create an ActivityAttributes called EXACTLY LiveActivitiesAppAttributes (if you rename, activity will be created but not appear!)

3. Runner 與 Widget Extension 都要新增 NSSupportsLiveActivities

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

4. 注意 Widget Extension 的 Minimum Deployments 要符合你的裝置,且要大於等於 16.1,否則完全不會有作用也不會有錯誤訊息!他預設是 17.1,這點我卡了最久,因為完全沒有訊息,點擊動態島又會正常回到 APP,所以一直以為是哪裡寫錯導致 UI 沒出來。


5. Cycle inside Runner

遇到 I have an issue when building my app on iOS: Error (Xcode): Cycle inside Runner; building could produce unreliable results.

調整 Runner 的 Build Phases:Copy Bundle Resources > Embed Foundation Extensions > Thin Binary

參考: https://stackoverflow.com/questions/77138968/handling-cycle-inside-runner-building-could-produce-unreliable-results-after-up/77178579#77178579


6. 程式更新與推播更新的資料取得方式不同

Flutter createActivityupdateActivity 給的 data 要用 sharedDefault.string(forKey: context.attributes.prefixedKey("key_name")) 取得

推播的資料取得需實作 LiveActivitiesAppAttributesContentState 才有資料

struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
    public typealias LiveDeliveryData = ContentState // don't forget to add this line, otherwise, live activity will not display it.

     public struct ContentState: Codable, Hashable { 
        let heading: String? 
     }
     
     var id = UUID()
}



extension LiveActivitiesAppAttributes {
  func prefixedKey(_ key: String) -> String {
    return "\(id)_\(key)"
  }
}

let sharedDefault = UserDefaults(suiteName: "group.joymap.com")!

struct LiveWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in
            // Flutter createActivity 跟 updateActivity 用這個拿
            let heading = sharedDefault.string(forKey: context.attributes.prefixedKey("heading"))!

            // 取得目前的時間戳,方便查看是否有收到推播
            let currentTimestamp = Date().timeIntervalSince1970
            
            // Lock screen/banner UI goes here
            VStack {
                Text(context.state.heading ?? "")
                Text("Update Timestamp: \(currentTimestamp)")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)

        }
......

推播更新我是使用 FCM 不走原生 APNs,測試時可用https://developers.google.com/oauthplayground 產生 Google OAuth Access Token。

結構文件: https://firebase.google.com/docs/cloud-messaging/ios/live-activity?hl=zh-TW 

FCM v1 messages:send Body

{
    "message":{
        "token": "<FCM_PUSH_TOKEN>",
        "notification":{
            "title":"test",
            "body":"test test"
        },
        "data":{},
        // Android
        "android": {
            "notification": {
                "notification_count": 3
            }
        },
        // iOS
        "apns": {
            "live_activity_token": "<https://pub.dev/packages/live_activities#update-live-activity-with-push-notification>",
            "headers": {
                "apns-priority": "5"
            },
            "payload": {
                "aps": {
                    "timestamp": {{$timestamp}},
                    "event": "update",
                    "content-state": {
                        "heading": ""
                    },
                    "alert": {
                        "title": "test",
                        "body": "test test"
                    },
                    "badge":4
                }
            }
        }
    }
}
// 從 Flutter live_activity_token 取得方式
// 開發者不建議使用,可能會隨時更新
final activityToken = await _liveActivitiesPlugin.getPushToken(_activityId!);

// 或是

_liveActivitiesPlugin.activityUpdateStream.listen((event) {
  event.map(
    active: (activity) {
      // Get the token
      print(activity.activityToken);
    },
    stale: (activity) {},
    ended: (activity) {},
    unknown: (activity) {},
  );
});

參考:
https://pub.dev/packages/live_activities
https://github.com/istornz/flutter_live_activities
https://github.com/istornz/flutter_live_activities/issues/93#issuecomment-2499572054
https://github.com/istornz/flutter_live_activities/issues/110#issuecomment-2558510013
https://firebase.google.com/docs/cloud-messaging/ios/live-activity?hl=zh-TW
https://stackoverflow.com/questions/77138968/handling-cycle-inside-runner-building-could-produce-unreliable-results-after-up/77178579#77178579