Per creare un'app Wear OS per la tua app Capacitor, aggiungi un modulo nativo Wear OS al android/ progetto, assegnagli lo stesso application ID e la stessa chiave di firma dell'app per smartphone e collega i due dispositivi con @capgo/capacitor-watch, che utilizza il Data Layer di Wear OS di Google su Android. Lo smartphone mantiene il suo code in TypeScript, mentre l'orologio esegue una piccola app Kotlin che scambia JSON con esso.
Questa guida illustra la configurazione del modulo, i percorsi esatti del Data Layer utilizzati dal plugin, l'app code Kotlin sull'orologio, l'app code TypeScript sullo smartphone, i test con gli emulatori e la pubblicazione su Google Play.
Come funziona il bridge da Capacitor a Wear OS
Wear OS non ha un runtime Capacitor, quindi l'app per l'orologio è nativa. Il plugin comunica tramite l'API del Data Layer (play-services-wearable) sul telefono, e l'app per l'orologio parla lo stesso protocollo.
| Metodo plugin / evento | Meccanismo Data Layer | Percorso |
|---|---|---|
sendMessage (da telefono a orologio) |
MessageClient |
/capgo/message |
messageReceived (da orologio a telefono) |
MessageClient |
/capgo/message |
messageReceivedWithReply |
MessageClient |
/capgo/message/withreply |
replyToMessage |
MessageClient |
/capgo/reply/{callbackId} |
updateApplicationContext / applicationContextReceived |
DataClient DataItem |
/capgo/context |
transferUserInfo / userInfoReceived |
DataClient DataItem |
/capgo/userinfo/{uuid} |
I messaggi trasportano JSON UTF-8 come byte grezzi. I DataItems trasportano lo stesso JSON come stringa sotto la chiave DataMap payload. Il telefono cerca l'app per smartwatch verificando la capability capgo_watch.
Conoscere questi percorsi è il segreto. Una volta che l'orologio li legge e li scrive, ogni metodo del plugin funziona.
Cosa ti serve
- Un'app Capacitor 8 con la piattaforma Android aggiunta (guida all'aggiornamento se sei rimasto indietro).
- Una versione recente di Android Studio con le immagini di sistema Wear OS installate.
- Un telefono con Google Play services. Il Data Layer ne dipende, quindi i telefoni senza Play services segnalano
isSupported: false. - Un orologio Wear OS o un emulatore accoppiato a un telefono Android. Gli orologi Wear OS accoppiati a un iPhone non possono raggiungere la tua app.
Passaggio 1: Installa il plugin
bun add @capgo/capacitor-watch
bunx cap sync android
Lo stesso pacchetto gestisce Apple Watch su iOS, quindi un'app cross-platform richiede un solo import. Vedi la docs del plugin per il riferimento completo e la nostra guida per Apple Watch per la parte iOS.
Passo 2: aggiungi un modulo Wear OS al progetto Android
Apri android/ in Android Studio (bunx cap open android) e usa File > Nuovo > Nuovo modulo > Wear OS. Lasciare che Android Studio generi il modulo ti dà file Gradle allineati alle tue versioni di Android Gradle Plugin e Kotlin, il che è più sicuro che incollare un file di build preso da un blog post. Dai un nome al modulo wear.
La procedura guidata aggiunge include ':wear' a android/settings.gradle. Mantienilo e non toccare le righe gestite da Capacitor.
Due regole che il Data Layer impone
- Uguale
applicationId. Inwear/build.gradle, impostaapplicationIdesattamente al valore inandroid/app/build.gradle. L'namespacepossono differire, l'application ID no. - Stesso certificato di firma. Le build di debug su una macchina condividono il debug keystore, quindi spesso funziona in locale ma si rompe in release. Firma la build release di wear con la stessa upload key dell'app per telefono.
Se una delle due regole viene violata, le due app non si vedranno mai e nulla in code potrà risolvere il problema.
Dipendenze e SDK minimo
Wear OS 3 e versioni successive si basano su API 30, quindi minSdk 30 rappresentano una base pratica per una nuova app per smartwatch. Aggiungi il Data Layer e gli helper per le coroutine:
dependencies {
implementation "com.google.android.gms:play-services-wearable:20.0.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0"
// Compose for Wear OS dependencies generated by the wizard stay as they are
}
Dichiara la capability
Crea wear/src/main/res/values/wear.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@array/android_wear_capabilities">
<string-array name="android_wear_capabilities" translatable="false">
<item>capgo_watch</item>
</string-array>
</resources>
La stringa deve essere capgo_watch, perché è ciò che il plugin interroga per calcolare isWatchAppInstalled.
Contrassegna l'app come app per smartwatch
In wear/src/main/AndroidManifest.xml, il wizard aggiunge <uses-feature android:name="android.hardware.type.watch" />. Decidi se l'app funziona senza il telefono. Un'app companion che mostra solo i dati del telefono deve dichiarare:
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
Passaggio 3: Ricevi i dati sullo smartwatch
Un WearableListenerService consente al sistema di consegnare gli eventi Data Layer anche quando l'interfaccia dell'orologio è chiusa. Registra il servizio con filtri per i /capgo path:
<service
android:name=".PhoneListenerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/capgo" />
</intent-filter>
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/capgo" />
</intent-filter>
</service>
Un piccolo store condiviso che l'UI può osservare:
package com.example.shop.wear
import kotlinx.coroutines.flow.MutableStateFlow
import org.json.JSONObject
object WatchStore {
val context = MutableStateFlow(JSONObject())
val lastMessage = MutableStateFlow(JSONObject())
val lastReply = MutableStateFlow(JSONObject())
}
Il servizio analizza ogni path:
package com.example.shop.wear
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService
import org.json.JSONObject
class PhoneListenerService : WearableListenerService() {
override fun onMessageReceived(event: MessageEvent) {
val json = JSONObject(String(event.data, Charsets.UTF_8))
when {
event.path == "/capgo/message" -> WatchStore.lastMessage.value = json
event.path.startsWith("/capgo/reply/") -> WatchStore.lastReply.value = json
}
}
override fun onDataChanged(events: DataEventBuffer) {
for (event in events) {
if (event.type != DataEvent.TYPE_CHANGED) continue
val path = event.dataItem.uri.path ?: continue
val payload = DataMapItem.fromDataItem(event.dataItem)
.dataMap.getString("payload") ?: "{}"
val json = JSONObject(payload)
when {
path == "/capgo/context" -> WatchStore.context.value = json
path.startsWith("/capgo/userinfo/") -> handleUserInfo(json)
}
}
}
private fun handleUserInfo(json: JSONObject) {
// Persist or act on queued events from the phone
}
}
Quando l'interfaccia dell'orologio si apre, leggi anche il contesto corrente direttamente in modo da mostrare i dati arrivati mentre l'app era chiusa:
suspend fun loadCurrentContext(context: android.content.Context) {
val items = Wearable.getDataClient(context).dataItems.await()
try {
for (item in items) {
if (item.uri.path == "/capgo/context") {
val payload = DataMapItem.fromDataItem(item).dataMap.getString("payload") ?: "{}"
WatchStore.context.value = JSONObject(payload)
}
}
} finally {
items.release()
}
}
Passo 4: Invia dati dall'orologio
L'orologio invia a ogni nodo telefono connesso. I messaggi sono per interazioni live, i DataItems per lo stato.
package com.example.shop.wear
import android.content.Context
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable
import kotlinx.coroutines.tasks.await
import org.json.JSONObject
import java.util.UUID
object PhoneLink {
suspend fun send(context: Context, data: JSONObject, expectReply: Boolean = false) {
val path = if (expectReply) "/capgo/message/withreply" else "/capgo/message"
val bytes = data.toString().toByteArray(Charsets.UTF_8)
val nodes = Wearable.getNodeClient(context).connectedNodes.await()
for (node in nodes) {
Wearable.getMessageClient(context).sendMessage(node.id, path, bytes).await()
}
}
suspend fun updateContext(context: Context, data: JSONObject) {
// Include a timestamp: identical DataItems are not redelivered
data.put("updatedAt", System.currentTimeMillis())
val request = PutDataMapRequest.create("/capgo/context").apply {
dataMap.putString("payload", data.toString())
setUrgent()
}.asPutDataRequest()
Wearable.getDataClient(context).putDataItem(request).await()
}
suspend fun queueEvent(context: Context, data: JSONObject) {
val request = PutDataMapRequest.create("/capgo/userinfo/${UUID.randomUUID()}").apply {
dataMap.putString("payload", data.toString())
setUrgent()
}.asPutDataRequest()
Wearable.getDataClient(context).putDataItem(request).await()
}
}
Quando il telefono riceve un /capgo/userinfo/... elemento, emette userInfoReceived e elimina l'elemento, in modo che la coda non cresca all'infinito.
Uno screen Compose minimale lo collega tutto:
@Composable
fun OrdersScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val state by WatchStore.context.collectAsState()
val reply by WatchStore.lastReply.collectAsState()
LaunchedEffect(Unit) { loadCurrentContext(context) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("${state.optInt("openOrders", 0)} open orders")
Button(onClick = {
scope.launch {
PhoneLink.send(context, JSONObject().put("action", "refresh"), expectReply = true)
}
}) { Text("Refresh") }
if (reply.has("count")) Text("Phone: ${reply.getInt("count")}")
}
}
Il telefono genera il callbackId, quindi l'orologio non può associare una risposta a una richiesta specifica. Se invii più richieste contemporaneamente, includi il tuo ID richiesta nel payload e restituiscilo nella risposta.
Passo 5: Comunica con l'orologio dal tuo Capacitor code
Il lato TypeScript è lo stesso code che scriveresti per Apple Watch:
import { Capacitor } from '@capacitor/core';
import { CapgoWatch } from '@capgo/capacitor-watch';
export async function initWearBridge(loadOpenOrders: () => Promise<number>) {
if (Capacitor.getPlatform() === 'web') return;
await CapgoWatch.addListener('messageReceivedWithReply', async ({ message, callbackId }) => {
if (message.action === 'refresh') {
const count = await loadOpenOrders();
await CapgoWatch.replyToMessage({ callbackId, data: { count } });
}
});
await CapgoWatch.addListener('applicationContextReceived', ({ context }) => {
console.log('watch state', context);
});
await CapgoWatch.addListener('userInfoReceived', ({ userInfo }) => {
console.log('queued event from watch', userInfo);
});
}
export async function syncToWatch(openOrders: number) {
const info = await CapgoWatch.getInfo();
if (!info.isSupported || !info.isWatchAppInstalled) return;
await CapgoWatch.updateApplicationContext({ context: { openOrders } });
if (info.isReachable) {
await CapgoWatch.sendMessage({ data: { toast: 'Orders updated' } });
}
}
Specifiche di Android da conoscere:
sendMessagerifiuta con “No connected Wear OS devices found” quando nessun nodo watch è connesso.isPairedeisReachableentrambi significano “almeno un nodo Wear OS è connesso”.isWatchAppInstalledsignifica che un nodo pubblicacapgo_watch.- La reachability non viene inviata come evento su Android oggi. Chiama
getInfo()quando hai bisogno di un valore aggiornato, ad esempio quando lo schermo riceve il focus. - Le risposte in sospeso scadono dopo cinque minuti sul lato telefono.
Quale canale usare
| Ti servono | Usa | Perché |
|---|---|---|
| Premi il pulsante Live, guarda l'app aprirsi | sendMessage / /capgo/message |
Veloce, ma perso se nessun nodo è connesso |
| Cosa deve mostrare l'orologio | updateApplicationContext / /capgo/context |
Latest value is kept and synced later |
| Eventi che non devono andare persi | transferUserInfo / /capgo/userinfo/{uuid} |
Ogni evento è un proprio DataItem |
Limiti del Data Layer
- I DataItem sono limitati a circa 100 KB. Anche i messaggi dovrebbero restare piccoli. Invia ID e recupera i contenuti dal tuo backend.
- Scrivere un DataItem con contenuto identico non genera un evento di modifica. Aggiungi un timestamp se ti serve un evento fresco.
- Un messaggio inviato allo smartwatch avvia la tua
WearableListenerService, non la tua UI. - Sul telefono, i listener del plugin esistono finché il processo dell'app Capacitor è in esecuzione. I messaggi inviati quando il processo è terminato non vengono riprodotti in JavaScript. I DataItems restano nel Data Layer, quindi usa il context per tutto ciò che il telefono deve vedere in seguito.
- I payload sono oggetti JSON. Gli array di primo livello e i valori non JSON non verranno parsati.
Test con emulatori e dispositivi
- Crea un emulatore telefonico con un'immagine di sistema Google Play e un emulatore Wear OS in Device Manager.
- Usa l'assistente di pairing Wear OS di Android Studio per accoppiarli. Installa l'app companion sull'emulatore del telefono.
- Esegui il
wearconfigurazione sull'emulatore dello smartwatch, poi avvia la tua app Capacitor sul telefono conbunx cap run android. - Chiama
CapgoWatch.getInfo()dal telefono.isWatchAppInstalled: trueconferma la corrispondenza di capability e package.
Per uno smartwatch fisico, abilita le opzioni sviluppatore e il debug ADB via Wi-Fi sull'orologio, quindi connettiti con adb connect <watch-ip>:<port>.
Risoluzione problemi
isSupported: false. Google Play services o il API di Wear OS manca sul telefono. Caso comune sugli emulatori senza immagini Play.
isWatchAppInstalled: false mentre l'orologio è connesso. Il capgo_watch la capability manca o è scritta male, oppure l'app wear non è installata su quell'orologio.
I messaggi non arrivano mai sull'orologio. Diversi applicationId o chiavi di firma diverse. Confronta ./gradlew :app:signingReport e ./gradlew :wear:signingReport.
Gli aggiornamenti del contesto smettono di arrivare. Stai scrivendo lo stesso payload ripetutamente. Aggiungi un campo che cambia.
La release build funziona in debug ma non su Play. Se usi Play App Signing, gli artifact per telefono e orologio devono essere firmati con la stessa chiave di firma app nella stessa app di Play Console.
R8 rimuove il tuo service. Mantieni le classi referenziate solo dal manifest se usi regole di shrinking personalizzate.
Pubblicazione su Google Play
L'app Wear OS viene pubblicata sotto la stessa app di Play Console dell'app per smartphone, poiché condividono il package name. Aggiungi il form factor Wear OS in Play Console, carica il bundle wear su una release track di Wear OS e aggiungi screenshot per orologio. Google valuta le app Wear OS in base alle sue linee guida di qualità Wear OS, quindi verifica i layout per schermi rotondi e le dimensioni dei touch target.
Per gli aggiornamenti, Live updates di Capgo puoi spingere fix al layer web dell'app per smartphone senza una release store, coprendo i bug nel tuo bridge TypeScript code. Le modifiche Kotlin sull'orologio passano da Google Play. Versiona i payload JSON in modo che un bundle smartphone più recente rimanga compatibile con una build orologio più vecchia. Se compili l'app per smartphone nel cloud, Build Capgo gestisce la build Android e la firma di Capacitor, e puoi generare un keystore corrispondente con il Generatore di keystore per Android.
In sintesi
Un companion Wear OS per un'app Capacitor si riduce a tre elementi: @capgo/capacitor-watch sullo smartphone, un modulo Kotlin che condivide l'application ID e la chiave di firma dello smartphone, e il /capgo/* i percorsi Data Layer in mezzo. Ottieni prima la capability e la firma giuste, poi il code è breve.