Pular para o conteúdo

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 to

Criaremos 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 to

Configuração do projeto Xcode

Anchor link to
  1. Abra seu projeto no Xcode e, na aba Geral, defina a implantação mínima para iOS 16.1
Aba Geral do Xcode mostrando a implantação mínima definida para iOS 16.1
  1. Em Info.plist, adicione uma nova chave NSSupportsLiveActivities e defina seu valor como YES (Booleano).

Info.plist

Anchor link to
<key>NSSupportsLiveActivities</key>
<true/>
  1. Na barra de ações, selecione File > New > Target e procure por WidgetExtension e crie um novo widget.

Implementação do lado do código

Anchor link to

Vamos 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 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))
}
}
}

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 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)
}
}

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 to

O 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);
}