Flutter Live Activity Setup

Guided implementation of a Live Activity for Flutter that can be started "in app" and updated via the REST API

Since Live Activities is both relatively new and specific to the iOS platform, there is not a lot of support built into the Flutter framework. The Live Activity must be implemented within native iOS and we will use Flutter’s Method Channels to interact with the Live Activity. There is a Flutter Package that can aid in this integration. However, to better understand and have the greatest flexibility, we will not use this package. The diagram below outlines the high-level components of how this comes together.

Blue = Your code, Gray = SDK

Blue = Your code, Gray = SDK

Setup

1. Create Flutter App

Create a new application using Flutter CLI, setting the app ID to com.example.flutterApplicationLa (commit).

2. Integrate SDK

Integrate the OneSignal SDK with the Flutter application (commit), following the instructions in the Flutter SDK Setup Guide.

3. Create Activity Widget

Code Example

  1. Open Runner’s Info.plist file in Xcode, add a new property, “Supports Live Activities”, and set it to “YES”.
  1. Select File > New > Target and search for “WidgetExtension” to create a new widget extension. Ensure “Include Live Activity” is checked. This will generate a few different files, the relevant files are WidgetExtensionBundle.swift, which identifies the widgets within the target, and WidgetExtensionLiveActivity.swift, which is the Live Activity widget.
  2. Select the WidgetExtensionLiveActivity.swift file and set its Target Membership to both “Runner” and “WidgetExtensionExtension”. The Runner target contains the code to start the live activity and must have visibility to WidgetExtensionAttributes.
  1. Update the contents of WidgetExtensionLiveActivity.swift with the following code.
import ActivityKit
import WidgetKit
import SwiftUI

struct WidgetExtensionAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var emoji: String
    }

    // Fixed non-changing properties about your activity go here!
    var name: String
}

@available(iOS 16.1, *)
struct WidgetExtensionLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: WidgetExtensionAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello \(context.attributes.name)")
                Text(context.state.emoji)
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)

        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI goes here.  Compose the expanded UI through
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom \(context.state.emoji)")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T \(context.state.emoji)")
            } minimal: {
                Text(context.state.emoji)
            }
            .widgetURL(URL(string: "http://www.onesignal.com"))
            .keylineTint(Color.red)
        }
    }
}

  1. Within the Runner target, create a new file called LiveActivityManager.swift.
  1. The LiveActivityManager is the native iOS implementation that will request a new Live Activity of type WidgetExtensionAttributes. The methods defined in LiveActivityManager will be exposed to Flutter through a method channel (Flutter-> native iOS). A “callback” method channel (native->iOS) is also created which will allow the Flutter app to be notified when the started Live Activity widget receives any updates. At a minimum, LiveActivityManager must notify the Flutter app of any new “push to update” tokens for a started Live Activity. LiveActivityManager.swift should be replaced with the following:
import Foundation

import ActivityKit
import Flutter
import Foundation

class LiveActivitiesManager {
    private static var callbackChannel: FlutterMethodChannel? = nil
    private static var managerChannel: FlutterMethodChannel? = nil
    
    public static func register(controller: FlutterViewController) {
        // Setup the iOS->Flutter channel to tell Flutter
        // when there is a new Live Activity update (push token)
        // The name here must be equivalent to the name for `_callbackMethodChannel` in `lib/live_activities_manager.dart`
        callbackChannel = FlutterMethodChannel(
                    name: "com.example.flutter_application_la/liveActivitiesCallback",
                    binaryMessenger: controller.binaryMessenger
                )
        
        // Setup the Flutter->iOS channel
      	// Currently supports `startLiveActivity` but can be expanded for more if needed.
        // The name here must be equivalent to the name for `_managerMethodChannel` in `lib/live_activities_manager.dart`
        managerChannel = FlutterMethodChannel(
                    name: "com.example.flutter_application_la/liveActivitiesManager",
                    binaryMessenger: controller.binaryMessenger
                )
        managerChannel?.setMethodCallHandler(handleMethodCall)
    }
    
    static func handleMethodCall(call: FlutterMethodCall, result: FlutterResult) {
        switch call.method {
          case "startLiveActivity":
              LiveActivitiesManager.startLiveActivity(
                  data: call.arguments as? Dictionary<String,Any> ?? [String: Any](),
                  result: result)
              break
          default:
              result(FlutterMethodNotImplemented)
        }
    }
    
    static func startLiveActivity(data: [String: Any], result: FlutterResult) {
        if #unavailable(iOS 16.1) {
            result(FlutterError(code: "1", message: "Live activity supported on 16.1 and higher", details: nil))
        }
        
        let attributes = WidgetExtensionAttributes(name: data["name"] as? String ?? "LA Title")
        
        let state = WidgetExtensionAttributes.ContentState(
            emoji: data["emoji"] as? String ?? "😀"
        )
        
        if #available(iOS 16.1, *) {
            do {
                let newActivity = try Activity<WidgetExtensionAttributes>.request(
                    attributes: attributes,
                    contentState: state,
                    pushType: .token)
                
                Task {
                    for await pushToken in newActivity.pushTokenUpdates {
                        let token = pushToken.map {String(format: "%02x", $0)}.joined()
                        callbackChannel?.invokeMethod("updatePushTokenCallback", arguments: ["activityId": data["activityId"], "token": token ])
                    }
                }
            } catch let error {
                result(FlutterError(code: "2", message: "Error requesting live activity", details: nil))
            }
        }
    }
}

  1. Update the AppDelegate didFinishLaunchingWithOptions method to drive the registering of the Live Activity channels:
override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
     
        // Register Flutter channels, specifically for Live Activities
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        LiveActivitiesManager.register(controller: controller)
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

Integrate native Live Activity Manager into Flutter

  1. On the Flutter side, we create an equivalent LiveActivitiesManager, which will be “the other side” of the 2 channels created on the native iOS side. LiveActivitiesManager.startLiveActivity will be called by the Flutter app whenever a live activity should be started, and will use the method channel to call the native iOS code. Likewise, when native iOS receives an updated token from the system, the _handleCallback method will be called, and the received token can be passed to OneSignal, allowing the LA to be updated via OneSignal. Create live_activities_manager.dart with the following code.
import 'dart:developer';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:onesignal_flutter/onesignal_flutter.dart';

// The flutter version of LiveActivitiesManager
// The counterparts of these channels are in `ios/Runner/LiveActivitiesManager.swift`
class LiveActivitiesManager {
  static const MethodChannel _managerMethodChannel = MethodChannel('com.example.flutter_application_la/liveActivitiesManager');
  static const MethodChannel _callbackMethodChannel = MethodChannel('com.example.flutter_application_la/liveActivitiesCallback');

  static register() {
    _callbackMethodChannel.setMethodCallHandler(_handleCallback);
  }

  static Future<Null> _handleCallback(MethodCall call) async {
    var args = call.arguments.cast<String, dynamic>();
    switch (call.method) {
      case 'updatePushTokenCallback':
        OneSignal.LiveActivities.enterLiveActivity(args["activityId"], args["token"]);
      default:
        log("Unrecognized callback method");
    }

    return null;
  }

  // start a live activity
 //  activityId is required for OneSignal
 //  name is static data rendered on the activity itself (see WidgetExtensionAttributes)
 //  emoji is dynamic data rendered on the activity itself (see WidgetExtensionAttributes). This can be updated continuously
 //        by OneSignal.
  static Future<void> startLiveActivity(String activityId, String name, String emoji) async {
    try {
      await _managerMethodChannel.invokeListMethod('startLiveActivity', {'activityId': activityId, 'name': name, 'emoji': emoji});
    } catch (e, st) {
      log(e.toString(), stackTrace: st);
    }
  }
}

  1. Add the final initialization code to main.dart.
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // Initialize the channels
    LiveActivitiesManager.register();
    
    // Other initialization code...

  1. Also, within main.dart updates are made to the UI to support starting a Live Activity via LiveActivitiesManager. Typically there is a button that will trigger the starting of a Live Activity:
void _startLiveActivity() {
  LiveActivitiesManager.startLiveActivity("your-activity-id","name-to-be-shown","emoji-to-be-shown");
}

Test Live Activity

  1. Start the Flutter app
  2. Enter a logical name for the Activity ID. As an example: activity_123. Click the + to start the Live Activity. You should see the Live Activity on the device’s lock screen.
  3. On a command prompt, send the following to OneSignal to update the Live Activity. Replace "<your_app_id>" with your OneSignal App ID, "<your_api_key>" with your OneSignal REST API Key, and potentially "activity_123" in the URL with the Activity ID you started the live activity under. The effect of this command should change the emoji on the Live Activity from 😀 to 🥳. The emoji parameter is just a string, feel free to put anything in there!
curl --location 'https://onesignal.com/api/v1/apps/<your_app_id>/live_activities/activity_123/notifications' \
--header 'Authorization: Basic: <your_api_key>' \
--header 'Content-Type: application/json' \
--data '{
    "event": "update",
    "event_updates": {
        "emoji": "🥳"
    },
    "name": "Some Logical Name For This Update"
}'


The full code example can be found on GitHub.