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 MethodChannel
dans un format de map 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;
/// 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'), ), ], ), ], ), ), ); }}
Implémentation côté natif
Anchor link toConfiguration du projet Xcode
Anchor link to- Ouvrez votre projet dans Xcode et dans l’onglet General, définissez le déploiement minimum sur iOS 16.1

- Dans
Info.plist
, ajoutez une nouvelle cléNSSupportsLiveActivities
et définissez sa valeur surYES
(Booléen).
Info.plist
Anchor link to<key>NSSupportsLiveActivities</key><true/>
- Depuis la barre d’actions, sélectionnez File > New > Target, recherchez
WidgetExtension
et créez un nouveau widget.
Implémentation du code
Anchor link toCréons une classe nommé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 de code). 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 qu’à créer l’interface utilisateur pour la Dynamic Island ! L’interface utilisateur complète pour la Dynamic Island doit être créée 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 de l’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 voilà ! 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 Pushwoosh fournit deux méthodes pour gérer la Live Activity dans le projet.
/// 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}); }
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", "sender_id": "XXXXXXXXXXXX"});/*** Call this method `defaultSetup()`*/ Pushwoosh.getInstance.defaultSetup();
Pendant l’initialisation de l’application, Pushwoosh enverra votre jeton push-to-start au serveur, ce qui vous permettra plus tard de démarrer la Live Activity via un appel d’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", // `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" } ] }}
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.
// 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); }