Para crear una app para Wear OS para tu app de Capacitor, añade un módulo nativo de Wear OS al android/ proyecto, dale el mismo application ID y la misma clave de firma que la app del móvil, y conecta ambos con @capgo/capacitor-watch, que utiliza la capa de datos de Wear OS de Google en Android. El teléfono mantiene su code en TypeScript, y el reloj ejecuta una pequeña app en Kotlin que intercambia JSON con él.
Esta guía explica la configuración del módulo, las rutas exactas de la capa de datos que usa el plugin, el code en Kotlin del reloj, el code en TypeScript del teléfono, las pruebas con emuladores y la publicación en Google Play.
Cómo funciona el bridge de Capacitor a Wear OS
Wear OS no tiene runtime de Capacitor, por lo que la app del reloj es nativa. El plugin habla el API de la Data Layer (play-services-wearable) en el teléfono, y la app del reloj habla el mismo protocolo.
| Método / evento del plugin | Mecanismo de la capa de datos | Ruta |
|---|---|---|
sendMessage (del teléfono al reloj) |
MessageClient |
/capgo/message |
messageReceived (del reloj al teléfono) |
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} |
Los mensajes llevan JSON UTF-8 como bytes sin procesar. Los DataItems llevan el mismo JSON como string en la clave DataMap payload. El teléfono comprueba si hay una app del reloj consultando la capacidad capgo_watch.
Conocer estas rutas es la clave. Una vez que el reloj las lee y escribe, todos los métodos de los plugins funcionan.
Qué necesitas
- Una app Capacitor 8 con la plataforma Android añadida (guía de actualización si estás desactualizado).
- Un Android Studio reciente con las imágenes de sistema de Wear OS instaladas.
- Un teléfono con Google Play services. La Data Layer depende de él, por lo que los teléfonos sin Play services reportan
isSupported: false. - Un reloj Wear OS o un emulador emparejado con un teléfono Android. Los relojes Wear OS emparejados con un iPhone no pueden acceder a tu app.
Paso 1: Instalar el plugin
bun add @capgo/capacitor-watch
bunx cap sync android
El mismo paquete maneja Apple Watch en iOS, por lo que una app multiplataforma necesita una sola importación. Consulta la docs del plugin para la referencia completa, y nuestra guía de Apple Watch para el lado de iOS.
Paso 2: Agregar un módulo de Wear OS al proyecto de Android
Abrir android/ en Android Studio (bunx cap open android) y usa Archivo > Nuevo > Nuevo módulo > Wear OS. Dejar que Android Studio genere el módulo te da archivos Gradle que coinciden con tu versión de Android Gradle Plugin y Kotlin, lo cual es más seguro que pegar un archivo de build de un blog. Nombra el módulo wear.
El asistente añade include ':wear' a android/settings.gradle. Mantenlo y no toques las líneas gestionadas por Capacitor.
Dos reglas que la Data Layer aplica
- Igual
applicationId. Enwear/build.gradle, estableceapplicationIdal valor exacto enandroid/app/build.gradle. Elnamespacepueden diferir, pero el ID de la aplicación no. - Mismo certificado de firma. Las builds de depuración en una misma máquina comparten el keystore de depuración, por lo que esto suele funcionar localmente y luego fallar en release. Firma la build de release para wear con la misma clave de subida que la app para teléfono.
Si se incumple alguna de estas reglas, las dos apps nunca se verán entre sí y nada en tu code lo solucionará.
Dependencias y SDK mínimo
Wear OS 3 y posteriores se basan en API 30, por lo que minSdk 30 es un límite práctico para una nueva app de reloj. Añade la Data Layer y los helpers de coroutinas:
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
}
Declara la capability
Crear 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 cadena debe ser capgo_watch, porque es lo que consulta el plugin para calcular isWatchAppInstalled.
Marca la app como app de reloj
En wear/src/main/AndroidManifest.xml, el asistente añade <uses-feature android:name="android.hardware.type.watch" />. Decide si la app funciona sin el teléfono. Una app complementaria que solo muestra datos del teléfono debe declarar:
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
Paso 3: Recibir datos en el reloj
Una WearableListenerService permite que el sistema entregue eventos de Data Layer incluso cuando la UI del reloj está cerrada. Regístralo con filtros para los /capgo paths:
<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 pequeño store compartido que la UI puede observar:
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())
}
El servicio parsea cada 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
}
}
Cuando se abre la UI del reloj, lee también el contexto actual directamente para que muestre los datos que llegaron mientras la app estaba cerrada:
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()
}
}
Paso 4: Enviar datos desde el reloj
El reloj envía a cada nodo de teléfono conectado. Los mensajes son para interacciones en vivo; los DataItems para el estado.
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()
}
}
Cuando el teléfono recibe un /capgo/userinfo/... item, emite userInfoReceived y elimina el elemento, para que la cola no crezca indefinidamente.
Una pantalla de Compose mínima lo integra todo:
@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")}")
}
}
El teléfono genera el callbackId, por lo que el reloj no puede asociar una respuesta con una solicitud concreta. Si lanzas varias solicitudes a la vez, incluye tu propio ID de solicitud en el payload y devuélvelo en la respuesta.
Paso 5: Habla con el reloj desde tu Capacitor code
La parte de TypeScript es la misma code que escribirías para 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' } });
}
}
Detalles de Android que conviene saber:
sendMessagefalla con “No connected Wear OS devices found” cuando no hay ningún nodo de reloj conectado.isPairedyisReachableambos significan “al menos un nodo Wear OS está conectado”.isWatchAppInstalledsignifica que un nodo anunciacapgo_watch.- Hoy, Reachability no se envía como evento en Android. Llama a
getInfo()cuando necesites un valor actualizado, por ejemplo, cuando la pantalla recupere el foco. - Las respuestas pendientes caducan a los cinco minutos en el lado del teléfono.
Qué canal usar
| ¿Necesitas | Usa | Por qué |
|---|---|---|
| Pulsa el botón Live, abre la app en el reloj | sendMessage / /capgo/message |
Rápido, pero se descarta si no hay un nodo conectado |
| Lo que el reloj debe mostrar | updateApplicationContext / /capgo/context |
Se conserva el último valor y se sincroniza después |
| Eventos que no deben perderse | transferUserInfo / /capgo/userinfo/{uuid} |
Cada evento es su propio DataItem |
Límites de la Data Layer
- Los DataItems están limitados a unos 100 KB. Los mensajes también deben ser pequeños. Envía IDs y obtén el contenido desde tu backend.
- Escribir un DataItem con contenido idéntico no dispara un evento de cambio. Añade una marca de tiempo si necesitas un evento nuevo.
- Un mensaje al reloj inicia tu
WearableListenerService, no tu UI. - En el teléfono, los listeners del plugin existen mientras el proceso de la app Capacitor está activo. Los mensajes enviados mientras está muerto no se reenvían a JavaScript. Los DataItems permanecen en la Data Layer, así que usa el contexto para todo lo que el teléfono deba ver más tarde.
- Los payloads son objetos JSON. No se procesarán arrays de nivel superior ni valores que no sean JSON.
Pruebas con emuladores y dispositivos
- Crea un emulador de teléfono con una imagen de sistema de Google Play y un emulador de Wear OS en el Device Manager.
- Usa el asistente de emparejamiento de Wear OS en Android Studio para vincularlos. Instala la app complementaria en el emulador del teléfono.
- Ejecuta la
wearconfiguración en el emulador del reloj, luego ejecuta tu app Capacitor en el teléfono conbunx cap run android. - Llama a
CapgoWatch.getInfo()desde el teléfono.isWatchAppInstalled: trueconfirma que la capacidad y el paquete coinciden.
Para un reloj físico, activa las opciones de desarrollador y la depuración ADB por Wi-Fi en el reloj, y luego conéctalo con adb connect <watch-ip>:<port>.
Solución de problemas
isSupported: false. Faltan Google Play services o el API de Wear OS en el teléfono. Es común en emuladores sin imágenes de Play.
isWatchAppInstalled: false mientras el reloj está conectado. El capgo_watch falta la capacidad o está mal escrita, o la app de Wear no está instalada en ese reloj.
Los mensajes nunca llegan al reloj. Diferentes applicationId o claves de firma distintas. Compara ./gradlew :app:signingReport y ./gradlew :wear:signingReport.
Las actualizaciones de contexto dejan de llegar. Estás escribiendo el mismo payload repetidamente. Añade un campo que cambie.
El release build funciona en debug pero no en Play. Si usas Play App Signing, los artefactos del teléfono y el reloj deben firmarse con la misma clave de firma de la app en la misma app de Play Console.
R8 elimina tu service. Mantén las clases referenciadas solo desde el manifest si usas reglas de shrinking personalizadas.
Publicar en Google Play
La app de Wear OS se publica en la misma app de Play Console que la app para teléfono, ya que comparten el nombre de paquete. Añade el factor de forma de Wear OS en Play Console, sube el bundle de wear a una pista de lanzamiento de Wear OS y añade capturas de pantalla del reloj. Google revisa las apps de Wear OS según sus directrices de calidad de Wear OS, así que verifica los diseños de pantalla redonda y los tamaños de los objetivos táctiles.
Para las actualizaciones, actualizaciones Live Update de Capgo puedes enviar correcciones a la capa web de la app para teléfono sin una publicación en la tienda, lo que cubre errores en tu puente TypeScript code. Los cambios en Kotlin del reloj pasan por Google Play. Versiona tus payloads JSON para que un bundle de teléfono más nuevo siga siendo compatible con una build de reloj más antigua. Si construyes la app para teléfono en la nube, Build de Capgo gestiona el build y la firma de Capacitor para Android, y puedes generar un keystore compatible con el Generador de keystore para Android.
Resumen
Una app companion de Wear OS para una app Capacitor se reduce a tres piezas: @capgo/capacitor-watch en el teléfono, un módulo Kotlin que comparte el application ID y la signing key del teléfono, y el /capgo/* Rutas de Data Layer intermedias. Primero obtén los permisos y la firma correctos, y luego el code será pan comido.