# Live Activity iOS in a Flutter project

We will create a Live Activity with a Stopwatch implementation for a Flutter project. The project will include a Flutter-side implementation as well as native implementation in the Xcode project.

## Flutter side implementation
We will create a class that will implement three methods:
- `startLiveActivity()`
- `updateLiveActivity()`
- `stopLiveActivity()`.

```dart
class DynamicIslandManager {
  final String channelKey;
  late final MethodChannel _methodChannel;

  DynamicIslandManager({required this.channelKey}) {
    _methodChannel = MethodChannel(channelKey);
  }

  Future<void> startLiveActivity({required Map<String, dynamic> jsonData}) async {
    try {
      await _methodChannel.invokeListMethod('startLiveActivity', jsonData);
    } catch (e, st) {
      log(e.toString(), stackTrace: st);
    }
  }

  Future<void> updateLiveActivity(
      {required Map<String, dynamic> jsonData}) async {
    try {
      await _methodChannel.invokeListMethod('updateLiveActivity', jsonData);
    } catch (e, st) {
      log(e.toString(), stackTrace: st);
    }
  }

  Future<void> stopLiveActivity() async {
    try {
      await _methodChannel.invokeListMethod('stopLiveActivity');
    } catch (e, st) {
      log(e.toString(), stackTrace: st);
    }
  }
}
```
We will be building a stopwatch app, and to pass the stopwatch timer data to native methods, we need to create a data model and send it to the method channel invocation in a JSON-like map format.

```dart
class DynamicIslandStopwatchDataModel {
  final int elapsedSeconds;

  DynamicIslandStopwatchDataModel({
    required this.elapsedSeconds,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'elapsedSeconds': elapsedSeconds,
    };
  }
}
```
Each method will include the necessary code to call the corresponding native methods.

This was the initial setup required to run it on the Flutter side. Following this, a basic stopwatch app UI with the necessary method calls was implemented.

```dart
class _StopWatchScreenState extends State<StopWatchScreen> {
  int seconds = 0;
  bool isRunning = false;
  Timer? timer;

  /// channel key is used to send data from flutter to swift side over
  /// a unique bridge (link between flutter & swift)
  final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');

  void startTimer() {
    setState(() {
      isRunning = true;
    });

    // invoking startLiveActivity Method
    diManager.startLiveActivity(
      jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(),
    );

    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        seconds++;
      });
            // invoking the updateLiveActivity Method
      diManager.updateLiveActivity(
        jsonData: DynamicIslandStopwatchDataModel(
          elapsedSeconds: seconds,
        ).toMap(),
      );
    });
  }

  void stopTimer() {
    timer?.cancel();
    setState(() {
      seconds = 0;
      isRunning = false;
    });

    // invoking the stopLiveActivity Method
    diManager.stopLiveActivity();
  }

  @override
  void dispose() {
    timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Stopwatch App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Stopwatch: $seconds seconds',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                ElevatedButton(
                  onPressed: isRunning ? null : startTimer,
                  child: const Text('Start'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: isRunning ? stopTimer : null,
                  child: const Text('Stop'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```
## Native side implementation

### Xcode project configuration

1. Open your project on Xcode and in General Tab set the minimum deployment to iOS 16.1
<img src="/flutter-flutter-ios-live-activities-1.webp" alt="Xcode General Tab showing minimum deployment set to iOS 16.1"/>

2. In `Info.plist` add a new key `NSSupportsLiveActivities` and set its value to `YES` (Boolean).

### Info.plist
```xml
<key>NSSupportsLiveActivities</key>
<true/>
```
3. From action bar, select **File > New > Target** and search for `WidgetExtensionand` create a new widget.

## Code side implementation
Let's create a class called LiveActivityManager that will manage the live activities for our Dynamic Island. This class will contain three methods: `startLiveActivity()`, `updateLiveActivity()`, and `stopLiveActivity()`.

**Swift**

```swift
import ActivityKit
import Flutter
import Foundation

@available(iOS 16.1, *)
class LiveActivityManager {

    private var stopwatchActivity: Activity<StopwatchWidgetAttributes>? = nil

    func startLiveActivity(data: [String: Any]?, result: FlutterResult) {
        let attributes = StopwatchWidgetAttributes()

        if let info = data {
            let state = StopwatchWidgetAttributes.ContentState(
                elapsedTime: info["elapsedSeconds"] as? Int ?? 0
            )
            stopwatchActivity = try? Activity<StopwatchWidgetAttributes>.request(
                attributes: attributes, contentState: state, pushType: nil)
        } else {
            result(FlutterError(code: "418", message: "Live activity didn't invoked", details: nil))
        }
    }

    func updateLiveActivity(data: [String: Any]?, result: FlutterResult) {
        if let info = data {
            let updatedState = StopwatchWidgetAttributes.ContentState(
                elapsedTime: info["elapsedSeconds"] as? Int ?? 0
            )

            Task {
                await stopwatchActivity?.update(using: updatedState)
            }
        } else {
            result(FlutterError(code: "418", message: "Live activity didn't updated", details: nil))
        }
    }

    func stopLiveActivity(result: FlutterResult) {
        do {
            Task {
                await stopwatchActivity?.end(using: nil, dismissalPolicy: .immediate)
            }
        } catch {
            result(FlutterError(code: "418", message: error.localizedDescription, details: nil))
        }
    }
}
```

Each method has its specific functionality. `startLiveActivity()` (as the name suggests) is responsible for initiating the Live Activity, which triggers the Dynamic Island functionality. Similarly, `stopLiveActivity()` and `updateLiveActivity()` handle stopping the Live Activity and updating the data displayed on the Dynamic Island.

Next, open `StopWatchDIWidgetLiveActivity.swift` and modify the `StopwatchDIWidgetAttributes` struct (as shown in the snippet). This attribute struct serves as the data model that holds the information to be displayed on the UI (directly from Flutter) and can also modify the UI as needed.

**Swift**

```swift
struct StopwatchWidgetAttributes: ActivityAttributes {
    public typealias stopwatchStatus = ContentState

    public struct ContentState: Codable, Hashable {
        var elapsedTime: Int
    }
}
```
Now, the only thing left is the UI for the Dynamic Island! (Woohoo, we've come this far!)
The entire UI for the Dynamic Island needs to be created using SwiftUI. For this article, a simple UI has been designed (but feel free to customize it as needed).

This UI code should be written inside the `StopwatchWidgetLiveActivity` struct. Remove the existing code from the struct, and you can follow the code below:

**SwiftUI**
```swift
struct StopwatchWidgetLiveActivity: Widget {

    func getTimeString(_ seconds: Int) -> String {
        let hours = seconds / 3600
        let minutes = (seconds % 3600) / 60
        let seconds = (seconds % 3600) % 60

        return hours == 0 ?
                String(format: "%02d:%02d", minutes, seconds)
                : String(format: "%02d:%02d:%02d", hours, minutes, seconds)
    }

    var body: some WidgetConfiguration {
        ActivityConfiguration(for: StopwatchWidgetAttributes.self) { context in
            HStack {
                Text("Time ellapsed")
                    .font(.system(size: 20, weight: .semibold))
                    .foregroundColor(.white)
                Spacer()
                Image(systemName: "timer")
                    .foregroundColor(.white)
                Spacer().frame(width: 10)
                Text(getTimeString(context.state.elapsedTime))
                    .font(.system(size: 24, weight: .semibold))
                    .foregroundColor(.yellow)
            }
            .padding(.horizontal)
            .activityBackgroundTint(Color.black.opacity(0.5))

        }
        dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.center) {
                    VStack(alignment: .center) {
                        Text("Pushwoosh Timer")
                        Spacer().frame(height: 24)
                        HStack {
                            Text("Time ellapsed")
                                .font(.system(size: 20, weight: .semibold))
                                .foregroundColor(.white)
                            Spacer()
                            Image(systemName: "timer")
                            Spacer().frame(width: 10)
                            Text(getTimeString(context.state.elapsedTime))
                                .font(.system(size: 24, weight: .semibold))
                                .foregroundColor(.yellow)
                        }.padding(.horizontal)
                    }
                }
            } compactLeading: {
                Image(systemName: "timer").padding(.leading, 4)
            } compactTrailing: {
                Text(getTimeString(context.state.elapsedTime)).foregroundColor(.yellow)
                    .padding(.trailing, 4)
            } minimal: {
                Image(systemName: "timer")
                    .foregroundColor(.yellow)
                    .padding(.all, 4)
            }
            .widgetURL(URL(string: "http://www.pushwoosh.com"))
            .keylineTint(Color.red)
        }
    }
}
```
Don't forget to also modify the `AppDelegate code`.

**AppDelegate.swift**
```swift
import UIKit
import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate {

    private let liveActivityManager: LiveActivityManager = LiveActivityManager()

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        GeneratedPluginRegistrant.register(with: self)

        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        let diChannel =  FlutterMethodChannel(name: "PW", binaryMessenger: controller.binaryMessenger)

        diChannel.setMethodCallHandler({ [weak self] (
            call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            switch call.method {
            case "startLiveActivity":
                self?.liveActivityManager.startLiveActivity(
                    data: call.arguments as? Dictionary<String,Any>,
                    result: result)
                break

            case "updateLiveActivity":
                self?.liveActivityManager.updateLiveActivity(
                    data: call.arguments as? Dictionary<String,Any>,
                    result: result)
                break

            case "stopLiveActivity":
                self?.liveActivityManager.stopLiveActivity(result: result)
                break

            default:
                result(FlutterMethodNotImplemented)
            }
        })

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}
```
And that's it! That covers everything needed to implement the Dynamic Island and link it to the Flutter code!

## Pushwoosh code implementation
The Pushwoosh Flutter plugin provides two methods for managing Live Activity in the project.

```dart
  /// Default setup Live Activity
  Future<void> defaultSetup() async {
    await _channel.invokeMethod("defaultSetup");
  }

  /// Default start Live Activity
  /// [activityId] activity ID
  /// [attributes] attributes
  /// [content] content
  Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async {
    await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content});
  }
```
The `defaultSetup()` method allows you to manage the structure of Live Activity and tokens on the Pushwoosh side.

Call this method during the application initialization.

```dart
  Pushwoosh.initialize({"app_id": "XXXXX-XXXXX"});
/**
* Call this method `defaultSetup()`
*/
  Pushwoosh.getInstance.defaultSetup();
```
During the initialization of the app, Pushwoosh will send your push-to-start token to the server, which will later allow you to start the Live Activity via an API call.
To start the Live Activity, you need to make an API request. Below is an example of the request:

```json
{
  "request": {
    "application": "XXXXX-XXXXX",
    "auth": "YOUR_AUTH_API_TOKEN",
    "notifications": [
      {
        "content": "Message",
        "title":"Title",
        "live_activity": {
          "event": "start", // `start` event
            "content-state": {
                "data": { // You need to pass the parameters in a dictionary with the key data.
                    "emoji": "dynamic data"
                }
            },
           "attributes-type": "DefaultLiveActivityAttributes",
            "attributes": {
                "data": {
                    "name": "static data"
                }
        }
        },
        "devices": [ "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" // HWID
       ],
        "live_activity_id": "your_activity_id"
      }
    ]
  }
}
```

[More detailed documentation on the API](/developer/api-reference/ios-live-activities-api)

If you want to start a simple Live Activity, for example, from your app, you can use the following method in Flutter: `defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)`

This method can be called on any event that triggers the start of the Live Activity.

```dart
// Function to start Live Activity
  void startLiveActivity() {
    // Create your activity ID
    String activityId = "stopwatch_activity";

    // Define the attributes you want to send to the Live Activity
    Map<String, dynamic> attributes = {
      'title': 'Stopwatch Activity',
      'description': 'This is a live activity for a stopwatch.'
    };

    // Define the content state to update on the Dynamic Island
    Map<String, dynamic> content = {
      'elapsedSeconds': 0
    };

    // Call Pushwoosh's defaultStart method to trigger the Live Activity
    Pushwoosh.getInstance().defaultStart(activityId, attributes, content);
  }
```