Live Activity для iOS в проекте Flutter
Мы создадим Live Activity с реализацией секундомера для проекта Flutter. Проект будет включать в себя реализацию как на стороне Flutter, так и нативную реализацию в проекте Xcode.
Реализация на стороне Flutter
Section titled “Реализация на стороне Flutter”Мы создадим класс, который будет реализовывать три метода:
startLiveActivity()updateLiveActivity()stopLiveActivity().
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); } }}Мы будем создавать приложение с секундомером, и для передачи данных таймера секундомера в нативные методы нам нужно создать модель данных и отправить ее при вызове канала методов в формате, подобном JSON-карте (map).
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}Каждый метод будет содержать необходимый код для вызова соответствующих нативных методов.
Это была первоначальная настройка, необходимая для запуска на стороне Flutter. После этого был реализован базовый UI приложения с секундомером и необходимыми вызовами методов.
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'), ), ], ), ], ), ), ); }}Нативная реализация
Section titled “Нативная реализация”Конфигурация проекта Xcode
Section titled “Конфигурация проекта Xcode”- Откройте ваш проект в Xcode и на вкладке General установите минимальную версию развертывания (minimum deployment) на iOS 16.1

- В файле
Info.plistдобавьте новый ключNSSupportsLiveActivitiesи установите его значение вYES(Boolean).
Info.plist
Section titled “Info.plist”<key>NSSupportsLiveActivities</key><true/>- В строке меню выберите File > New > Target, найдите
Widget Extensionи создайте новый виджет.
Реализация кода
Section titled “Реализация кода”Создадим класс LiveActivityManager, который будет управлять Live Activities для нашего Dynamic Island. Этот класс будет содержать три метода: startLiveActivity(), updateLiveActivity() и stopLiveActivity().
Swift
import ActivityKitimport Flutterimport 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)) } }}Каждый метод имеет свою специфическую функциональность. startLiveActivity() (как следует из названия) отвечает за запуск Live Activity, что активирует функциональность Dynamic Island. Аналогично, stopLiveActivity() и updateLiveActivity() отвечают за остановку Live Activity и обновление данных, отображаемых на Dynamic Island.
Далее откройте файл StopWatchDIWidgetLiveActivity.swift и измените структуру StopwatchDIWidgetAttributes (как показано в сниппете). Эта структура атрибутов служит моделью данных, которая хранит информацию для отображения в UI (непосредственно из Flutter), а также может изменять UI при необходимости.
Swift
struct StopwatchWidgetAttributes: ActivityAttributes { public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable { var elapsedTime: Int }}Теперь осталось только создать UI для Dynamic Island! (Ура, мы почти у цели!) Весь пользовательский интерфейс для Dynamic Island должен быть создан с использованием SwiftUI. Для этой статьи был разработан простой UI (но вы можете настроить его по своему усмотрению).
Этот код UI должен быть написан внутри структуры StopwatchWidgetLiveActivity. Удалите существующий код из структуры и следуйте приведенному ниже коду:
SwiftUI
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("Wavesend 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://wavesend.ru")) .keylineTint(Color.red) } }}Не забудьте также изменить код AppDelegate.
AppDelegate.swift
import UIKitimport 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) }}Вот и все! Это все, что нужно для реализации Dynamic Island и его связывания с кодом Flutter!
Реализация кода Wavesend
Section titled “Реализация кода Wavesend”Плагин Wavesend для Flutter предоставляет два метода для управления Live Activity в проекте.
/// 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}); }Метод defaultSetup() позволяет управлять структурой Live Activity и токенами на стороне Wavesend.
Вызывайте этот метод во время инициализации приложения.
Wavesend.initialize({"app_id": "XXXXX-XXXXX", "sender_id": "XXXXXXXXXXXX"});/*** Call this method `defaultSetup()`*/ Wavesend.getInstance.defaultSetup();Во время инициализации приложения Wavesend отправит ваш push-to-start токен на сервер, что в дальнейшем позволит запускать Live Activity через вызов API. Чтобы запустить Live Activity, вам необходимо сделать API-запрос. Ниже приведен пример запроса:
{ "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" } ] }}Более подробная документация по API
Если вы хотите запустить простой Live Activity, например, из вашего приложения, вы можете использовать следующий метод в Flutter: defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)
Этот метод можно вызывать при любом событии, которое инициирует запуск Live Activity.
// 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 Wavesend's defaultStart method to trigger the Live Activity Wavesend.getInstance().defaultStart(activityId, attributes, content); }