Live Activity iOS dans un projet Flutter
Nous allons créer une Live Activity avec une implémentation de chronomètre pour un projet Flutter. Le projet inclura une implémentation côté Flutter ainsi qu’une implémentation native dans le projet Xcode.
Implémentation côté Flutter
Anchor link toNous allons créer une classe qui implémentera trois méthodes :
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); } }}Nous allons construire une application de chronomètre, et pour transmettre les données du minuteur du chronomètre aux méthodes natives, nous devons créer un modèle de données et l’envoyer à l’invocation du canal de méthode dans un format de carte de type JSON.
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}Chaque méthode inclura le code nécessaire pour appeler les méthodes natives correspondantes.
C’était la configuration initiale requise pour l’exécuter côté Flutter. Ensuite, une interface utilisateur basique d’application de chronomètre avec les appels de méthode nécessaires a été implémentée.
class _StopWatchScreenState extends State<StopWatchScreen> { int seconds = 0; bool isRunning = false; Timer? timer;
/// la clé de canal est utilisée pour envoyer des données du côté Flutter vers le côté Swift via /// un pont unique (lien entre Flutter & Swift) final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');
void startTimer() { setState(() { isRunning = true; });
// invocation de la méthode startLiveActivity diManager.startLiveActivity( jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(), );
timer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { seconds++; }); // invocation de la méthode updateLiveActivity diManager.updateLiveActivity( jsonData: DynamicIslandStopwatchDataModel( elapsedSeconds: seconds, ).toMap(), ); }); }
void stopTimer() { timer?.cancel(); setState(() { seconds = 0; isRunning = false; });
// invocation de la méthode stopLiveActivity 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'), ), ], ), ], ), ), ); }}Implémentation côté natif
Anchor link toConfiguration du projet Xcode
Anchor link to- Ouvrez votre projet sur Xcode et dans l’onglet Général, définissez le déploiement minimum sur iOS 16.1

- Dans
Info.plist, ajoutez une nouvelle cléNSSupportsLiveActivitieset définissez sa valeur surYES(Booléen).
Info.plist
Anchor link to<key>NSSupportsLiveActivities</key><true/>- Dans la barre d’actions, sélectionnez Fichier > Nouveau > Cible et recherchez
WidgetExtensionandpuis créez un nouveau widget.
Implémentation du code
Anchor link toCréons une classe appelée LiveActivityManager qui gérera les activités en direct pour notre Dynamic Island. Cette classe contiendra trois méthodes : startLiveActivity(), updateLiveActivity() et 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)) } }}Chaque méthode a sa fonctionnalité spécifique. startLiveActivity() (comme son nom l’indique) est responsable du lancement de la Live Activity, ce qui déclenche la fonctionnalité Dynamic Island. De même, stopLiveActivity() et updateLiveActivity() gèrent l’arrêt de la Live Activity et la mise à jour des données affichées sur la Dynamic Island.
Ensuite, ouvrez StopWatchDIWidgetLiveActivity.swift et modifiez la structure StopwatchDIWidgetAttributes (comme montré dans l’extrait). Cette structure d’attributs sert de modèle de données qui contient les informations à afficher sur l’interface utilisateur (directement depuis Flutter) et peut également modifier l’interface utilisateur si nécessaire.
Swift
struct StopwatchWidgetAttributes: ActivityAttributes { public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable { var elapsedTime: Int }}Maintenant, il ne reste plus que l’interface utilisateur pour la Dynamic Island ! (Youhou, nous sommes arrivés jusqu’ici !) L’ensemble de l’interface utilisateur pour la Dynamic Island doit être créé avec SwiftUI. Pour cet article, une interface utilisateur simple a été conçue (mais n’hésitez pas à la personnaliser selon vos besoins).
Ce code d’interface utilisateur doit être écrit à l’intérieur de la structure StopwatchWidgetLiveActivity. Supprimez le code existant de la structure, et vous pouvez suivre le code ci-dessous :
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("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) } }}N’oubliez pas de modifier également le code 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) }}Et c’est tout ! Cela couvre tout ce qui est nécessaire pour implémenter la Dynamic Island et la lier au code Flutter !
Implémentation du code Pushwoosh
Anchor link toLe plugin Flutter de Pushwoosh fournit deux méthodes pour gérer la Live Activity dans le projet.
/// Configuration par défaut de la Live Activity Future<void> defaultSetup() async { await _channel.invokeMethod("defaultSetup"); }
/// Démarrage par défaut de la Live Activity /// [activityId] ID de l'activité /// [attributes] attributs /// [content] contenu Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async { await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content}); }La méthode defaultSetup() vous permet de gérer la structure de la Live Activity et les jetons côté Pushwoosh.
Appelez cette méthode lors de l’initialisation de l’application.
Pushwoosh.initialize({"app_id": "XXXXX-XXXXX"});/*** Appelez cette méthode `defaultSetup()`*/ Pushwoosh.getInstance.defaultSetup();Pendant l’initialisation de l’application, Pushwoosh enverra votre jeton de démarrage par push au serveur, ce qui vous permettra plus tard de démarrer la Live Activity via un appel API. Pour démarrer la Live Activity, vous devez effectuer une requête API. Voici un exemple de la requête :
{ "request": { "application": "XXXXX-XXXXX", "auth": "YOUR_AUTH_API_TOKEN", "notifications": [ { "content": "Message", "title":"Title", "live_activity": { "event": "start", // événement `start` "content-state": { "data": { // Vous devez passer les paramètres dans un dictionnaire avec la clé 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" } ] }}Documentation plus détaillée sur l’API
Si vous souhaitez démarrer une Live Activity simple, par exemple, depuis votre application, vous pouvez utiliser la méthode suivante dans Flutter : defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)
Cette méthode peut être appelée sur n’importe quel événement qui déclenche le démarrage de la Live Activity.
// Fonction pour démarrer la Live Activity void startLiveActivity() { // Créez votre ID d'activité String activityId = "stopwatch_activity";
// Définissez les attributs que vous souhaitez envoyer à la Live Activity Map<String, dynamic> attributes = { 'title': 'Stopwatch Activity', 'description': 'This is a live activity for a stopwatch.' };
// Définissez l'état du contenu à mettre à jour sur la Dynamic Island Map<String, dynamic> content = { 'elapsedSeconds': 0 };
// Appelez la méthode defaultStart de Pushwoosh pour déclencher la Live Activity Pushwoosh.getInstance().defaultStart(activityId, attributes, content); }