Creazione di un'app Wear OS
Copia un prompt di configurazione con i passaggi di installazione e la guida markdown completa per questo plugin.
Questa guida ti spiega come creare un'app companion per Wear OS che comunica con la tua app Capacitor su smartphone tramite gli stessi percorsi Data Layer utilizzati CapgoWatchPlugin su Android (play-services-wearable).
Prerequisiti
Sezione intitolata “Prerequisiti”Prima di iniziare, assicurati di avere:
- Android Studio (ultima versione stabile)
- Un progetto Android Capacitor esistente (
npx cap add android(se necessario) - Uno smartwatch Wear OS fisico associato al telefono (consigliato per test end-to-end)
- La stessa signing key per le release build dell'app del telefono e del modulo Wear OS
Come il plugin si mappa su Wear OS
Sezione: “Come il plugin si mappa su Wear OS”Il plugin lato smartphone (CapgoWatchPlugin.java) utilizza queste rotte Data Layer:
| Rotta | Tipo | Scopo |
|---|---|---|
/capgo/message | Messaggio | Messaggi unidirezionali (da telefono a watch e viceversa) |
/capgo/message/withreply | Messaggio | Messaggi da watch a telefono che richiedono una risposta JS tramite replyToMessage |
/capgo/reply/{callbackId} | Messaggio | Payload di risposta dal telefono al watch |
/capgo/context | DataItem | Ultimo contesto applicativo (payload string in formato JSON) |
/capgo/userinfo/{uuid} | DataItem | Info utente in coda (payload string in formato JSON) |
Il telefono rileva l'app per il watch con la capgo_watch capability (WATCH_APP_CAPABILITY nel plugin).
Struttura del progetto
Sezione intitolata “Project structure”Dopo aver completato questa guida, la struttura Android sarà questa:
Directoryandroid/
Directoryapp/ (modulo telefono, host Capacitor)
- …
Directorywear/ (nuovo modulo Wear OS)
Directorysrc/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
Passo 1: Aggiungi un modulo Wear OS
Sezione: “Passaggio 1: Aggiungi un modulo Wear OS”-
Apri la
androidcartella in Android Studio (non la root del repo). -
Usa File → Nuovo → Nuovo modulo…
-
Seleziona Wear OS → App Wear OS vuota (o App Wear OS con Compose, se preferisci).
-
Configura il modulo:
- Nome del modulo:
wear(o il nome che preferisci) - Nome del pacchetto: deve corrispondere al namespace dell'app per telefono, se possibile
- Minimo SDK: allinealo al tuo progetto (il plugin
minSdkè 24)
- Nome del modulo:
-
Clicca su Fine e attendi il sync di Gradle.
Passo 2: Dipendenze Gradle
Sezione intitolata “Step 2: Gradle dependencies”Il plugin Capacitor dipende già da com.google.android.gms:play-services-wearable:18.2.0. La tua wear il modulo richiede lo stesso API.
In wear/build.gradle (o wear/build.gradle.kts), aggiungi:
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}Sincronizza Gradle dopo la modifica.
Assicurati che settings.gradle includa il modulo wear:
include ':app'include ':wear'Esegui npx cap sync android dal tuo progetto Capacitor quando modifichi i moduli nativi, quindi esegui il rebuild in Android Studio.
Passaggio 3: Dichiara la capgo_watch capacità
Sezione: “Passaggio 3: Dichiara la capability capgo_watch”Crea wear/src/main/res/values/wear.xml:
<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"> <string-array name="android_wear_capabilities" translatable="false" tools:ignore="UnusedResources"> <item>capgo_watch</item> </string-array></resources>Questo permette al plugin del telefono di impostare isWatchAppInstalled quando interroga la capability capgo_watch.
Passo 4: Manifest del modulo Wear e application ID
Sezione: “Passaggio 4: Manifest del modulo Wear e application ID”Usa lo stesso applicationId nell'app del telefono e nel modulo Wear. Impostalo in wear/build.gradle (e assicurati che coincida in android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}Esempio wear/src/main/AndroidManifest.xml skeleton:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.type.watch" />
<application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" android:theme="@android:style/Theme.DeviceDefault">
<activity android:name=".WearMainActivity" android:exported="true" android:taskAffinity="" android:theme="@android:style/Theme.DeviceDefault"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>Passo 5: Bridge lato Wear (Kotlin)
Sezione intitolata “Passaggio 5: Bridge lato Wear (Kotlin)”Crea CapgoWearBridge.kt nel tuo pacchetto wear. Specchia i percorsi in CapgoWatchPlugin.java.
Il plugin per telefono assegna un callbackId per ogni /capgo/message/withreply e invia la risposta su /capgo/reply/{callbackId}. La watch non vede mai quell'ID in anticipo. Per richieste concorrenti, includi un requestId nel tuo payload JSON, restituiscilo dal telefono in replyToMessage, e gestisci i key handler sull'orologio in base a quel campo.
package com.example.myapp.wear
import android.content.Contextimport android.net.Uriimport android.util.Logimport com.google.android.gms.wearable.CapabilityClientimport com.google.android.gms.wearable.DataClientimport com.google.android.gms.wearable.DataEventimport com.google.android.gms.wearable.DataEventBufferimport com.google.android.gms.wearable.DataMapItemimport com.google.android.gms.wearable.MessageClientimport com.google.android.gms.wearable.MessageEventimport com.google.android.gms.wearable.PutDataMapRequestimport com.google.android.gms.wearable.Wearableimport org.json.JSONObjectimport java.nio.charset.StandardCharsetsimport java.util.UUIDimport java.util.concurrent.ConcurrentHashMapimport java.util.concurrent.Executorsimport java.util.concurrent.TimeUnit
class CapgoWearBridge(private val context: Context) : MessageClient.OnMessageReceivedListener, DataClient.OnDataChangedListener {
companion object { private const val TAG = "CapgoWearBridge" const val CAPABILITY = "capgo_watch" const val PATH_MESSAGE = "/capgo/message" const val PATH_MESSAGE_WITH_REPLY = "/capgo/message/withreply" const val PATH_REPLY_PREFIX = "/capgo/reply/" const val PATH_CONTEXT = "/capgo/context" const val PATH_USER_INFO_PREFIX = "/capgo/userinfo/" }
private val executor = Executors.newSingleThreadExecutor() private val timeoutScheduler = Executors.newSingleThreadScheduledExecutor() private val messageClient = Wearable.getMessageClient(context) private val dataClient = Wearable.getDataClient(context) private val nodeClient = Wearable.getNodeClient(context)
var onMessage: ((JSONObject) -> Unit)? = null var onContext: ((JSONObject) -> Unit)? = null var onUserInfo: ((JSONObject) -> Unit)? = null
fun start() { messageClient.addListener(this) dataClient.addListener(this) Wearable.getCapabilityClient(context) .addLocalCapability(CAPABILITY) }
fun stop() { messageClient.removeListener(this) dataClient.removeListener(this) pendingReplyTimeouts.values.forEach { it.cancel(false) } pendingReplyTimeouts.clear() pendingReplyHandlers.clear() timeoutScheduler.shutdownNow() }
fun sendMessage(data: JSONObject, onError: (String) -> Unit = {}) { executor.execute { try { val nodes = com.google.android.gms.tasks.Tasks.await(nodeClient.connectedNodes) val payload = data.toString().toByteArray(StandardCharsets.UTF_8) for (node in nodes) { messageClient.sendMessage(node.id, PATH_MESSAGE, payload).let { com.google.android.gms.tasks.Tasks.await(it) } } } catch (e: Exception) { Log.e(TAG, "sendMessage failed", e) onError(e.message ?: "sendMessage failed") } } }
private val pendingReplyHandlers = ConcurrentHashMap<String, (JSONObject) -> Unit>() private val pendingReplyTimeouts = ConcurrentHashMap<String, java.util.concurrent.ScheduledFuture<*>>() private val outboundUserInfoPaths = ConcurrentHashMap.newKeySet<String>()
fun sendMessageWithReply( data: JSONObject, onReply: (JSONObject) -> Unit, onError: (String) -> Unit = {}, ) { executor.execute { val requestId = UUID.randomUUID().toString() try { val nodes = com.google.android.gms.tasks.Tasks.await(nodeClient.connectedNodes) if (nodes.isEmpty()) { onError("No connected phone node") return@execute } val payloadJson = JSONObject(data.toString()).apply { put("requestId", requestId) } pendingReplyHandlers[requestId] = onReply val timeout = timeoutScheduler.schedule( { if (pendingReplyHandlers.remove(requestId) != null) { pendingReplyTimeouts.remove(requestId) onError("Reply timed out after 5 minutes") } }, 5, TimeUnit.MINUTES, ) pendingReplyTimeouts[requestId] = timeout val node = nodes.first() com.google.android.gms.tasks.Tasks.await( messageClient.sendMessage( node.id, PATH_MESSAGE_WITH_REPLY, payloadJson.toString().toByteArray(StandardCharsets.UTF_8), ), ) } catch (e: Exception) { pendingReplyHandlers.remove(requestId) pendingReplyTimeouts.remove(requestId)?.cancel(false) Log.e(TAG, "sendMessageWithReply failed", e) onError(e.message ?: "sendMessageWithReply failed") } } }
fun updateApplicationContext(contextData: JSONObject, onError: (String) -> Unit = {}) { executor.execute { try { val request = PutDataMapRequest.create(PATH_CONTEXT) request.dataMap.putString("payload", contextData.toString()) request.setUrgent() com.google.android.gms.tasks.Tasks.await(dataClient.putDataItem(request.asPutDataRequest())) } catch (e: Exception) { Log.e(TAG, "updateApplicationContext failed", e) onError(e.message ?: "updateApplicationContext failed") } } }
fun transferUserInfo(userInfo: JSONObject, onError: (String) -> Unit = {}) { executor.execute { val path = PATH_USER_INFO_PREFIX + UUID.randomUUID() try { outboundUserInfoPaths.add(path) val request = PutDataMapRequest.create(path) request.dataMap.putString("payload", userInfo.toString()) request.setUrgent() com.google.android.gms.tasks.Tasks.await(dataClient.putDataItem(request.asPutDataRequest())) } catch (e: Exception) { outboundUserInfoPaths.remove(path) Log.e(TAG, "transferUserInfo failed", e) onError(e.message ?: "transferUserInfo failed") } } }
override fun onMessageReceived(event: MessageEvent) { val path = event.path val json = try { JSONObject(String(event.data, StandardCharsets.UTF_8)) } catch (e: Exception) { Log.w(TAG, "Ignoring non-JSON message on $path", e) return }
when { path == PATH_MESSAGE -> onMessage?.invoke(json) path.startsWith(PATH_REPLY_PREFIX) -> { val requestId = json.optString("requestId", "") if (requestId.isNotEmpty()) { pendingReplyTimeouts.remove(requestId)?.cancel(false) pendingReplyHandlers.remove(requestId)?.invoke(json) } } } }
override fun onDataChanged(dataEvents: DataEventBuffer) { for (event in dataEvents) { if (event.type != DataEvent.TYPE_CHANGED) continue val item = event.dataItem val path = item.uri.path ?: continue val map = DataMapItem.fromDataItem(item).dataMap val payload = map.getString("payload", "{}") val json = try { JSONObject(payload) } catch (e: Exception) { Log.w(TAG, "Ignoring non-JSON DataItem on $path", e) continue }
when { path == PATH_CONTEXT -> onContext?.invoke(json) path.startsWith(PATH_USER_INFO_PREFIX) -> { if (outboundUserInfoPaths.remove(path)) { continue } onUserInfo?.invoke(json) dataClient.deleteDataItems(item.uri) } } } }
}Collega il bridge in WearMainActivity.kt:
class WearMainActivity : ComponentActivity() { private lateinit var bridge: CapgoWearBridge
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) bridge = CapgoWearBridge(applicationContext) bridge.onMessage = { json -> Log.d("Wear", "phone message: $json") } bridge.onContext = { json -> Log.d("Wear", "context: $json") } bridge.onUserInfo = { json -> Log.d("Wear", "userInfo: $json") } bridge.start() setContent { /* your Compose UI */ } }
override fun onDestroy() { bridge.stop() super.onDestroy() }}Usa applicationContext quando crei i client, così i listener sopravvivono ai cambi di configurazione.
Passo 6: Capacitor lato phone (TypeScript)
Sezione: “Passo 6: Capacitor lato telefono (TypeScript)”Installa e sincronizza il plugin nell'app del telefono (come su iOS):
import { CapgoWatch } from '@capgo/capacitor-watch';
export async function setupWatchListeners() { await CapgoWatch.addListener('messageReceived', (event) => { console.log('Wear message:', event.message); });
await CapgoWatch.addListener('messageReceivedWithReply', async (event) => { const requestId = event.message.requestId; await CapgoWatch.replyToMessage({ callbackId: event.callbackId, data: { requestId, ok: true, echo: event.message }, }); });
await CapgoWatch.addListener('applicationContextReceived', (event) => { console.log('Wear context:', event.context); });
await CapgoWatch.addListener('userInfoReceived', (event) => { console.log('Wear userInfo:', event.userInfo); });}
export async function pingWatch() { const info = await CapgoWatch.getInfo(); if (!info.isSupported) { console.warn('Wear OS APIs not available on this device'); return; } if (!info.isReachable) { console.warn('No connected Wear OS nodes'); return; } await CapgoWatch.sendMessage({ data: { action: 'ping', at: Date.now() }, });}Chiama setupWatchListeners() all'inizio del bootstrap dell'app (ad esempio dopo platform.ready()).
Passo 7: Build, install e test
Sezione intitolata “Step 7: Build, install, and test”-
Seleziona la telefono
appconfigurazione di esecuzione, installa su un telefono Android fisico. -
Seleziona la
wearconfigurazione di esecuzione, installa sullo smartwatch associato (o su un emulatore di smartwatch collegato all'emulatore del telefono). -
Verifica
getInfo()sul telefono:isSupported: truequando i Google Play services e Wearable API sono disponibiliisReachable: truequando almeno un nodo è connessoisWatchAppInstalled: truequando l'orologio trasmettecapgo_watch
-
Invia
CapgoWatch.sendMessagedal telefono e verificabridge.onMessagesull'orologio. -
Invia dall'orologio con
sendMessageWithReplye confermamessageReceivedWithReplypiùreplyToMessageconsegna su/capgo/reply/{callbackId}. -
Test
updateApplicationContextetransferUserInfoin entrambe le direzioni usando i percorsi DataItem sopra.
Risoluzione problemi
Sezione intitolata “Troubleshooting”isSupported: false su un telefono reale
- Google Play services mancanti o obsoleti
- API Wearable non disponibile sul dispositivo
isReachable: false ma l'orologio è accoppiato
- Bluetooth disattivato o app dell'orologio non avviata di recente
- Diversi
applicationIdo chiavi di firma diverse tra i moduli - Nessun nodo Wear OS connesso ai Google Play services
isWatchAppInstalled: false
- APK companion Wear non installato sull'orologio
- Mancante
capgo_watchvoce inwear.xml - Capability non sincronizzata: reinstalla l'APK Wear dopo aver modificato le capability
I messaggi non arrivano mai
- Il percorso deve corrispondere esattamente a (
/capgo/message, non un prefisso personalizzato) - Il payload deve essere un testo JSON in UTF-8 (il plugin usa
JSONObject/JSObjectstringhe)
La risposta non torna mai all'orologio
- Il telefono deve chiamare
replyToMessagecon ilcallbackIddamessageReceivedWithReply - Includi lo stesso
requestIdnel JSON di risposta se l'orologio correla richieste concorrenti - Le risposte in sospeso scadono dopo 5 minuti su Android (costante del plugin
PENDING_REPLY_TTL_MS)
Prossimi passi
Sezione intitolata “Prossimi passi”- Creazione di un'app watchOS per Apple Watch e CapgoWatchSDK
- Guida rapida per API condivisi e note sulla piattaforma
- Esempi per i pattern completi dell'app
- Codice sorgente del plugin per le costanti del percorso lato telefono
Continua da Creare un'app Wear OS
Sezione intitolata “Continua da Creazione di un'app Wear OS”Se stai usando Creazione di un'app Wear OS per pianificare il lavoro sui plugin nativi, collegalo a Utilizzo di @capgo/capacitor-watch per la funzionalità nativa in Utilizzo di @capgo/capacitor-watch, Directory dei plugin di Capgo per il workflow del prodotto nella Capgo Plugin Directory, Plugin Capacitor di Capgo per i dettagli di implementazione in Capacitor Plugins di Capgo, Aggiunta o aggiornamento dei plugin per i dettagli su come aggiungere o aggiornare i plugin, e Alternative ai plugin Ionic Enterprise per il workflow del prodotto in Alternative ai plugin Ionic Enterprise.