Live Activity do iOS em um projeto Flutter
Criaremos uma Live Activity com uma implementação de cronômetro para um projeto Flutter. O projeto incluirá uma implementação do lado do Flutter, bem como uma implementação nativa no projeto Xcode.
Implementação do lado do Flutter
Anchor link toCriaremos uma classe que implementará três métodos:
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); } }}Estaremos construindo um aplicativo de cronômetro e, para passar os dados do timer do cronômetro para os métodos nativos, precisamos criar um modelo de dados e enviá-lo para a invocação do canal de método em um formato de mapa semelhante a JSON.
class DynamicIslandStopwatchDataModel { final int elapsedSeconds;
DynamicIslandStopwatchDataModel({ required this.elapsedSeconds, });
Map<String, dynamic> toMap() { return <String, dynamic>{ 'elapsedSeconds': elapsedSeconds, }; }}Cada método incluirá o código necessário para chamar os métodos nativos correspondentes.
Esta foi a configuração inicial necessária para executá-lo no lado do Flutter. Em seguida, foi implementada uma interface de usuário básica do aplicativo de cronômetro com as chamadas de método necessárias.
class _StopWatchScreenState extends State<StopWatchScreen> { int seconds = 0; bool isRunning = false; Timer? timer;
/// a chave do canal é usada para enviar dados do flutter para o lado swift por meio de /// uma ponte única (link entre flutter e swift) final DynamicIslandManager diManager = DynamicIslandManager(channelKey: 'PW');
void startTimer() { setState(() { isRunning = true; });
// invocando o método startLiveActivity diManager.startLiveActivity( jsonData: DynamicIslandStopwatchDataModel(elapsedSeconds: 0).toMap(), );
timer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { seconds++; }); // invocando o método updateLiveActivity diManager.updateLiveActivity( jsonData: DynamicIslandStopwatchDataModel( elapsedSeconds: seconds, ).toMap(), ); }); }
void stopTimer() { timer?.cancel(); setState(() { seconds = 0; isRunning = false; });
// invocando o método 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'), ), ], ), ], ), ), ); }}Implementação do lado nativo
Anchor link toConfiguração do projeto Xcode
Anchor link to- Abra seu projeto no Xcode e, na aba Geral, defina a implantação mínima para iOS 16.1

- Em
Info.plist, adicione uma nova chaveNSSupportsLiveActivitiese defina seu valor comoYES(Booleano).
Info.plist
Anchor link to<key>NSSupportsLiveActivities</key><true/>- Na barra de ações, selecione File > New > Target e procure por
WidgetExtensione crie um novo widget.
Implementação do lado do código
Anchor link toVamos criar uma classe chamada LiveActivityManager que gerenciará as atividades ao vivo para nossa Dynamic Island. Esta classe conterá três métodos: startLiveActivity(), updateLiveActivity() e 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)) } }}Cada método tem sua funcionalidade específica. startLiveActivity() (como o nome sugere) é responsável por iniciar a Live Activity, o que aciona a funcionalidade da Dynamic Island. Da mesma forma, stopLiveActivity() e updateLiveActivity() lidam com a interrupção da Live Activity e a atualização dos dados exibidos na Dynamic Island.
Em seguida, abra StopWatchDIWidgetLiveActivity.swift e modifique a struct StopwatchDIWidgetAttributes (conforme mostrado no trecho de código). Esta struct de atributo serve como o modelo de dados que contém as informações a serem exibidas na UI (diretamente do Flutter) e também pode modificar a UI conforme necessário.
Swift
struct StopwatchWidgetAttributes: ActivityAttributes { public typealias stopwatchStatus = ContentState
public struct ContentState: Codable, Hashable { var elapsedTime: Int }}Agora, a única coisa que falta é a UI para a Dynamic Island! (Uhu, chegamos até aqui!) Toda a UI para a Dynamic Island precisa ser criada usando SwiftUI. Para este artigo, uma UI simples foi projetada (mas sinta-se à vontade para personalizá-la conforme necessário).
Este código de UI deve ser escrito dentro da struct StopwatchWidgetLiveActivity. Remova o código existente da struct e você pode seguir o código abaixo:
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ão se esqueça de modificar também o código do 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) }}E é isso! Isso cobre tudo o que é necessário para implementar a Dynamic Island e vinculá-la ao código Flutter!
Implementação do código Pushwoosh
Anchor link toO plugin Flutter da Pushwoosh fornece dois métodos para gerenciar a Live Activity no projeto.
/// Configuração padrão da Live Activity Future<void> defaultSetup() async { await _channel.invokeMethod("defaultSetup"); }
/// Início padrão da Live Activity /// [activityId] ID da atividade /// [attributes] atributos /// [content] conteúdo Future<void> defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content) async { await _channel.invokeMethod("defaultStart", {"activityId": activityId, "attributes": attributes, "content": content}); }O método defaultSetup() permite que você gerencie a estrutura da Live Activity e os tokens no lado da Pushwoosh.
Chame este método durante a inicialização do aplicativo.
Pushwoosh.initialize({"app_id": "XXXXX-XXXXX", "sender_id": "XXXXXXXXXXXX"});/*** Chame este método `defaultSetup()`*/ Pushwoosh.getInstance.defaultSetup();Durante a inicialização do aplicativo, a Pushwoosh enviará seu token push-to-start para o servidor, o que permitirá posteriormente iniciar a Live Activity por meio de uma chamada de API. Para iniciar a Live Activity, você precisa fazer uma solicitação de API. Abaixo está um exemplo da solicitação:
{ "request": { "application": "XXXXX-XXXXX", "auth": "YOUR_AUTH_API_TOKEN", "notifications": [ { "content": "Message", "title":"Title", "live_activity": { "event": "start", // evento `start` "content-state": { "data": { // Você precisa passar os parâmetros em um dicionário com a chave 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" } ] }}Documentação mais detalhada sobre a API
Se você quiser iniciar uma Live Activity simples, por exemplo, a partir do seu aplicativo, pode usar o seguinte método no Flutter: defaultStart(String activityId, Map<String, dynamic> attributes, Map<String, dynamic> content)
Este método pode ser chamado em qualquer evento que acione o início da Live Activity.
// Função para iniciar a Live Activity void startLiveActivity() { // Crie o ID da sua atividade String activityId = "stopwatch_activity";
// Defina os atributos que você deseja enviar para a Live Activity Map<String, dynamic> attributes = { 'title': 'Stopwatch Activity', 'description': 'This is a live activity for a stopwatch.' };
// Defina o estado do conteúdo para atualizar na Dynamic Island Map<String, dynamic> content = { 'elapsedSeconds': 0 };
// Chame o método defaultStart da Pushwoosh para acionar a Live Activity Pushwoosh.getInstance().defaultStart(activityId, attributes, content); }