Saltar al contenido

Crear una app para Wear OS

GitHub

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

Antes de empezar, asegúrate de tener:

  • Android Studio (última versión estable)
  • Un proyecto de Android Capacitor existente (npx cap add android si 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

El plugin en el teléfono (CapgoWatchPlugin.java) usa estas rutas de Data Layer:

RutaTipoPropósito
/capgo/messageMensajeMensajes unidireccionales (del teléfono al reloj y viceversa)
/capgo/message/withreplyMensajeMensajes del reloj al teléfono que esperan una respuesta JS vía replyToMessage
/capgo/reply/{callbackId}MensajePayload de respuesta del teléfono de vuelta al reloj
/capgo/contextDataItemÚltimo contexto de la app (payload string con JSON)
/capgo/userinfo/{uuid}DataItemInfo 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).

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
  1. Abre el android carpeta en Android Studio (no la raíz del repo).

  2. Elige Archivo → Nuevo → Nuevo módulo…

  3. Selecciona Wear OS → App de Wear vacía (o App para Wear OS con Compose si lo prefieres).

  4. 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 minSdk es 24)
  5. Haz clic en Finalizar y deja que Gradle sincronice.

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.

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.

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>

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.Context
import android.net.Uri
import android.util.Log
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.DataClient
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.MessageClient
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable
import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import 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()).

  1. Selecciona la teléfono app configuración de ejecución, instalar en un teléfono Android físico.

  2. Selecciona la wear configuración de ejecución, instalar en el reloj vinculado (o en un emulador de reloj conectado al emulador del teléfono).

  3. Confirma getInfo() en el teléfono:

    • isSupported: true cuando Google Play services y Wearable API estén disponibles
    • isReachable: true cuando al menos un nodo esté conectado
    • isWatchAppInstalled: true cuando el reloj emite la señal capgo_watch
  4. Enviar CapgoWatch.sendMessage desde el teléfono y verificar bridge.onMessage en el reloj.

  5. Enviar desde el reloj con sendMessageWithReply y confirmar messageReceivedWithReply más replyToMessage se entrega en /capgo/reply/{callbackId}.

  6. Probar updateApplicationContext y transferUserInfo en ambas direcciones usando las rutas DataItem de arriba.

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 applicationId o 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_watch entrada en wear.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 / JSObject strings)

La respuesta nunca regresa al reloj

  • El teléfono debe llamar replyToMessage con el callbackId de messageReceivedWithReply
  • Incluye el mismo requestId en 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)

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.