Crear una app para Wear OS
Copia un prompt de configuración con los pasos de instalación y la guía completa en markdown para este plugin.
Esta guía te enseña a crear una app complementaria para Wear OS que se comunica con tu app de Capacitor mediante las mismas rutas de Data Layer que CapgoWatchPlugin en Android (play-services-wearable).
Requisitos
Sección titulada “Requisitos previos”Antes de empezar, asegúrate de tener:
- Android Studio (última versión estable)
- Un proyecto de Android Capacitor existente (
npx cap add androidsi es necesario) - Un reloj Wear OS físico vinculado al teléfono (recomendado para pruebas end-to-end)
- La misma clave de firma para la app del teléfono y los builds de release del módulo Wear OS
Cómo se mapea el plugin a Wear OS
Sección titulada “Cómo el plugin se mapea a Wear OS”El plugin en el teléfono (CapgoWatchPlugin.java) usa estas rutas de Data Layer:
| Ruta | Tipo | Propósito |
|---|---|---|
/capgo/message | Mensaje | Mensajes unidireccionales (del teléfono al reloj y viceversa) |
/capgo/message/withreply | Mensaje | Mensajes del reloj al teléfono que esperan una respuesta JS vía replyToMessage |
/capgo/reply/{callbackId} | Mensaje | Payload de respuesta del teléfono de vuelta al reloj |
/capgo/context | DataItem | Último contexto de la app (payload string con JSON) |
/capgo/userinfo/{uuid} | DataItem | Info de usuario en cola (payload string con JSON) |
El teléfono detecta tu app de reloj con la capgo_watch capacidad (WATCH_APP_CAPABILITY en el plugin).
Estructura del proyecto
Sección titulada “Project structure”Tras completar esta guía, tu árbol de Android se verá así:
Directorioandroid/
Directorioapp/ (módulo de teléfono, host Capacitor)
- …
Directoriowear/ (nuevo módulo Wear OS)
Directoriosrc/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
Paso 1: Añadir un módulo Wear OS
Sección titulada “Paso 1: Añadir un módulo Wear OS”-
Abre el
androidcarpeta en Android Studio (no la raíz del repo). -
Elige Archivo → Nuevo → Nuevo módulo…
-
Selecciona Wear OS → App de Wear vacía (o App para Wear OS con Compose si lo prefieres).
-
Configura el módulo:
- Nombre del módulo:
wear(o el nombre que prefieras) - Nombre del paquetecoincide con el namespace de tu app móvil cuando sea posible
- Mínimo SDK: ajústalo a tu proyecto (el plugin
minSdkes 24)
- Nombre del módulo:
-
Haz clic en Finalizar y deja que Gradle sincronice.
Paso 2: Dependencias de Gradle
Sección titulada “Step 2: Gradle dependencies”El plugin Capacitor ya depende de com.google.android.gms:play-services-wearable:18.2.0. Tu wear módulo necesita el mismo API.
En wear/build.gradle (o wear/build.gradle.kts), añade:
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}Sincroniza Gradle tras editar.
Ensure settings.gradle incluye el módulo wear:
include ':app'include ':wear'Ejecuta npx cap sync android de tu proyecto Capacitor al cambiar módulos nativos, y luego reconstruye en Android Studio.
Paso 3: Declara la capgo_watch capacidad
Sección titulada “Paso 3: Declara la capacidad capgo_watch”Crear 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>Esto permite que el plugin del teléfono establezca isWatchAppInstalled cuando consulta la capacidad capgo_watch.
Paso 4: Manifest del módulo Wear y application ID
Sección titulada “Paso 4: Manifiesto del módulo Wear e ID de aplicación”Usa el mismo applicationId en la app del teléfono y el módulo Wear. Configúralo en wear/build.gradle (y haz que coincida en android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}Ejemplo 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>Paso 5: Bridge del lado Wear (Kotlin)
Sección titulada “Paso 5: Bridge en Wear OS (Kotlin)”Crear CapgoWearBridge.kt en tu paquete de wear. Refleja las rutas en CapgoWatchPlugin.java.
El plugin del teléfono asigna un callbackId para cada /capgo/message/withreply y envía la respuesta en /capgo/reply/{callbackId}. El reloj nunca ve ese ID de antemano. Para peticiones concurrentes, incluye un requestId en tu payload JSON, haz un echo desde el teléfono en replyToMessage, y usa ese campo para manejar los eventos en el reloj.
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) } } } }
}Conecta el bridge en 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 al construir los clientes para que los listeners sobrevivan a cambios de configuración.
Paso 6: Capacitor del lado del móvil (TypeScript)
Sección titulada “Paso 6: Capacitor en el teléfono (TypeScript)”Instala y sincroniza el plugin en la app del teléfono (igual que en 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() }, });}Llama setupWatchListeners() al inicio del bootstrap de tu app (por ejemplo, tras platform.ready()).
Paso 7: Build, instalación y pruebas
Sección titulada “Paso 7: Build, install, and test”-
Selecciona la teléfono
appconfiguración de ejecución, instalar en un teléfono Android físico. -
Selecciona la
wearconfiguración de ejecución, instalar en el reloj vinculado (o en un emulador de reloj conectado al emulador del teléfono). -
Confirma
getInfo()en el teléfono:isSupported: truecuando Google Play services y Wearable API estén disponiblesisReachable: truecuando al menos un nodo esté conectadoisWatchAppInstalled: truecuando el reloj emite la señalcapgo_watch
-
Enviar
CapgoWatch.sendMessagedesde el teléfono y verificarbridge.onMessageen el reloj. -
Enviar desde el reloj con
sendMessageWithReplyy confirmarmessageReceivedWithReplymásreplyToMessagese entrega en/capgo/reply/{callbackId}. -
Probar
updateApplicationContextytransferUserInfoen ambas direcciones usando las rutas DataItem de arriba.
Solución de problemas
Sección titulada “Solución de problemas”isSupported: false en un teléfono real
- faltan los Google Play services o están desactualizados
- API de Wearable no disponible en el dispositivo
isReachable: false pero el reloj está vinculado
- Bluetooth desactivado o la app del reloj no se ha ejecutado recientemente
- Diferentes
applicationIdo las llaves de firma entre módulos - Ningún nodo de Wear OS conectado a Google Play services
isWatchAppInstalled: false
- El APK de la app complementaria no está instalado en el reloj
- Falta la
capgo_watchentrada enwear.xml - Capability no sincronizada: reinstala el APK de Wear tras cambiar las capabilities
Los mensajes nunca llegan
- La ruta debe coincidir exactamente (
/capgo/message, no un prefijo personalizado) - El payload debe ser texto JSON en UTF-8 (el plugin usa
JSONObject/JSObjectstrings)
La respuesta nunca regresa al reloj
- El teléfono debe llamar
replyToMessagecon elcallbackIddemessageReceivedWithReply - Incluye el mismo
requestIden el JSON de respuesta si el reloj correlaciona peticiones concurrentes - Las respuestas pendientes expiran tras 5 minutos en Android (constante del plugin
PENDING_REPLY_TTL_MS)
Próximos pasos
Sección titulada “Próximos pasos”- Crear una app para watchOS para Apple Watch y CapgoWatchSDK
- Primeros pasos para API compartidos y notas de plataforma
- Ejemplos para patrones de app completos
- Código fuente del plugin para constantes de ruta en el teléfono
Continúa desde Crear una app para Wear OS
Sección titulada “Continúa desde Crear una app para Wear OS”Si estás usando Crear una app para Wear OS para planificar el trabajo del plugin nativo, conéctalo con Uso de @capgo/capacitor-watch para la capacidad nativa en Uso de @capgo/capacitor-watch, Directorio de plugins de Capgo para el flujo de trabajo de productos en el Capgo Plugin Directory, Plugins de Capacitor por Capgo para los detalles de implementación en Plugins de Capacitor por Capgo, Añadir o actualizar plugins para el detalle de implementación en Agregar o actualizar plugins, y Alternativas a los plugins de Ionic Enterprise para el flujo de trabajo del producto en Alternativas a plugins de Ionic Enterprise.