Pour créer une application Wear OS pour votre application Capacitor, ajoutez un module natif Wear OS au android/ projet, donnez-lui le même ID d'application et la même clé de signature que l'application mobile, et connectez les deux avec @capgo/capacitor-watch, qui utilise la Data Layer de Google Wear OS sur Android. Le téléphone conserve son code TypeScript, tandis que la montre exécute une petite application Kotlin qui échange du JSON avec lui.
Ce guide détaille la configuration du module, les chemins Data Layer exacts utilisés par le plugin, le code Kotlin sur la montre, le code TypeScript sur le téléphone, les tests avec des émulateurs et la publication sur Google Play.
Fonctionnement du pont Capacitor vers Wear OS
Wear OS n'a pas de runtime Capacitor, l'application côté montre est donc native. Le plugin utilise le protocole Data Layer API (play-services-wearable) sur le téléphone, et votre application côté montre utilise le même protocole.
| Méthode / événement du plugin | Mécanisme Data Layer | Chemin |
|---|---|---|
sendMessage (du téléphone vers la montre) |
MessageClient |
/capgo/message |
messageReceived (de la montre vers le téléphone) |
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} |
Les messages transportent du JSON UTF-8 sous forme d'octets bruts. Les DataItems transportent ce même JSON sous forme de chaîne via la clé DataMap payload. Le téléphone cherche une application pour montre en vérifiant la capability capgo_watch.
Connaître ces chemins est toute l'astuce. Une fois que la montre peut les lire et les écrire, chaque méthode de plugin fonctionne.
Ce dont vous avez besoin
- Une application Capacitor 8 avec la plateforme Android ajoutée (guide de mise à jour si vous n'êtes pas à jour).
- Une version récente d'Android Studio avec les images système Wear OS installées.
- Un téléphone avec les services Google Play. La Data Layer en dépend, donc les téléphones sans services Play signalent
isSupported: false. - Une montre Wear OS ou un émulateur couplé à un téléphone Android. Les montres Wear OS couplées à un iPhone ne peuvent pas atteindre votre application.
Étape 1 : Installer le plugin
bun add @capgo/capacitor-watch
bunx cap sync android
Le même package gère l'Apple Watch sur iOS : une app multiplateforme n'a donc besoin que d'un seul import. Voir le doc du plugin pour la référence complète, et notre guide Apple Watch pour la partie iOS.
Étape 2 : Ajouter un module Wear OS au projet Android
Ouvrez android/ dans Android Studio (bunx cap open android) et utilisez Fichier > Nouveau > Nouveau module > Wear OS. Laisser Android Studio générer le module vous donne des fichiers Gradle alignés sur vos versions de l'Android Gradle Plugin et de Kotlin, ce qui est plus sûr que de coller un fichier de build récupéré dans un article de blog. Nommez le module wear.
L'assistant ajoute include ':wear' vers android/settings.gradle. Conservez-l’et ne modifiez pas les lignes gérées par Capacitor.
Deux règles imposées par la Data Layer
- Idem
applicationId. Danswear/build.gradle, définissezapplicationIdà la valeur exacte indiquée dansandroid/app/build.gradle. Lenamespacepeuvent différer, mais pas l'ID de l'application. - Même certificat de signature. Les builds de debug sur une même machine partagent le keystore de debug, ce qui fonctionne souvent en local avant de casser en release. Signez le build release Wear avec la même clé d'upload que l'app téléphone.
Si l'une de ces règles n'est pas respectée, les deux apps ne se verront jamais et rien dans votre code ne pourra le corriger.
Dépendances et SDK minimum
Wear OS 3 and later are based on API 30, so minSdk 30 est un minimum pratique pour une nouvelle application de montre. Ajoutez la Data Layer et les helpers de coroutines :
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
}
Déclarer la capacité
Créer 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 chaîne doit être capgo_watch, car c’est ce que le plugin interroge pour calculer isWatchAppInstalled.
Marquer l'app comme application montre
Dans wear/src/main/AndroidManifest.xml, l’assistant ajoute <uses-feature android:name="android.hardware.type.watch" />. Décidez si l’application fonctionne sans le téléphone. Un compagnon qui n’affiche que des données du téléphone doit déclarer :
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
Étape 3 : Recevoir des données sur la montre
Un WearableListenerService permet au système de délivrer les événements de la Data Layer même lorsque l’interface de la montre est fermée. Enregistrez-l’avec des filtres pour /capgo chemins :
<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 petit store partagé que l’interface peut observer :
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())
}
Le service analyse chaque chemin :
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
}
}
Lorsque l’interface de la montre s’ouvre, lisez également le contexte actuel directement afin d’afficher les données arrivées pendant que l’application était fermée :
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()
}
}
Étape 4 : Envoyer des données depuis la montre
La montre envoie à chaque nœud de téléphone connecté. Les messages servent aux interactions en direct, les DataItems à l’état.
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()
}
}
Lorsque le téléphone reçoit un /capgo/userinfo/... item, il émet userInfoReceived et supprime l'élément, pour que la file ne grossisse pas indéfiniment.
Un écran Compose minimal permet de tout assembler :
@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")}")
}
}
Le téléphone génère le callbackId, de sorte que la montre ne peut pas associer une réponse à une requête spécifique. Si vous envoyez plusieurs requêtes en même temps, incluez votre propre identifiant de requête dans la charge utile et renvoyez-le dans la réponse.
Étape 5 : Communiquer avec la montre depuis votre Capacitor code
Le côté TypeScript est le même code que vous écririez pour 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' } });
}
}
Spécificités Android à connaître :
sendMessagerejette avec « No connected Wear OS devices found » lorsqu'aucun nœud de montre n'est connecté.isPairedetisReachablesignifient tous les deux « au moins un nœud Wear OS est connecté ».isWatchAppInstalledsignifie qu'un nœud publiecapgo_watch.- La disponibilité n'est pas poussée en tant qu'événement sur Android pour le moment. Appelez
getInfo()lorsque vous avez besoin d'une valeur à jour, par exemple lorsque l'écran reprend le focus. - Les réponses en attente expirent après cinq minutes côté téléphone.
Quel canal utiliser
| Besoin | Utiliser | Pourquoi |
|---|---|---|
| Appui en direct, app de la montre ouverte | sendMessage / /capgo/message |
Rapide, mais perdu si aucun nœud n'est connecté |
| Ce que la montre doit afficher | updateApplicationContext / /capgo/context |
Latest value is kept and synced later |
| Événements qui ne doivent pas être perdus | transferUserInfo / /capgo/userinfo/{uuid} |
Chaque événement est son propre DataItem |
Limites de la couche de données
- Les DataItems sont limités à environ 100 Ko. Les messages doivent aussi rester légers. Envoyez des IDs et récupérez le contenu depuis votre backend.
- L'écriture d'un DataItem avec un contenu identique ne déclenche pas d'événement de changement. Ajoutez un timestamp si vous avez besoin d'un nouvel événement.
- Un message envoyé à la montre déclenche
WearableListenerService, pas votre interface. - Sur le téléphone, les listeners du plugin existent tant que le processus de l'application Capacitor est actif. Les messages envoyés pendant qu'il est inactif ne sont pas rejoués vers JavaScript. Les DataItems restent dans la Data Layer, utilisez donc le contexte pour tout ce que le téléphone doit voir plus tard.
- Les payloads sont des objets JSON. Les tableaux de premier niveau et les valeurs non JSON ne sont pas analysés.
Tests avec émulateurs et appareils
- Créez un émulateur de téléphone avec une image système Google Play et un émulateur Wear OS dans le Device Manager.
- Utilisez l'assistant d'appairage Wear OS d'Android Studio pour les appairer. Il installe l'application compagnon sur l'émulateur de téléphone.
- Exécutez la
wearconfiguration sur l'émulateur de montre, puis lancez votre application Capacitor sur le téléphone avecbunx cap run android. - Appelez
CapgoWatch.getInfo()depuis le téléphone.isWatchAppInstalled: trueconfirme que la capacité et le package correspondent.
Pour une montre physique, activez les options développeur et le débogage ADB via Wi-Fi sur la montre, puis connectez-vous avec adb connect <watch-ip>:<port>.
Troubleshooting
isSupported: false. Les services Google Play ou l’API de Wear OS sont manquants sur le téléphone. Fréquent sur les émulateurs sans images Play.
isWatchAppInstalled: false pendant qu'une montre est connectée. Le capgo_watch la capability est manquante ou mal orthographiée, ou l'application Wear n'est pas installée sur cette montre.
Les messages n'arrivent jamais sur la montre. Différentes applicationId ou des clés de signature différentes. Comparez ./gradlew :app:signingReport et ./gradlew :wear:signingReport.
Les mises à jour de contexte ne parviennent plus. Vous écrivez le même payload à répétition. Ajoutez un champ variable.
Le build de release fonctionne en debug mais pas sur Play. Si vous utilisez Play App Signing, les artefacts de la montre et du téléphone doivent être signés par la même clé de signature d'application dans la même application Play Console.
R8 supprime votre service. Conservez les classes référencées uniquement depuis le manifeste si vous utilisez des règles de shrinking personnalisées.
Publication sur Google Play
L'application Wear OS est publiée sous la même application Play Console que l'application téléphone, car elles partagent le même nom de package. Ajoutez le facteur de forme Wear OS dans Play Console, envoyez le bundle wear sur une piste de release Wear OS et ajoutez des captures d'écran de la montre. Google évalue les applications Wear OS selon ses directives de qualité Wear OS, donc vérifiez les mises en page pour écrans ronds et les tailles des cibles tactiles.
Pour les mises à jour, les live updates Capgo peut envoyer des correctifs à la couche web de l'application téléphone sans release sur le store, ce qui couvre les bugs de votre bridge TypeScript code. Les modifications Kotlin sur la montre passent par Google Play. Versionnez vos payloads JSON pour qu'un bundle téléphone plus récent reste compatible avec un build montre plus ancien. Si vous compilez l'application téléphone dans le cloud, Build Capgo gère le build et la signature Android de Capacitor, et vous pouvez générer un keystore correspondant avec le générateur de keystore Android en ligne.
Récapitulatif
Un compagnon Wear OS pour une application Capacitor repose sur trois éléments : @capgo/capacitor-watch sur le téléphone, un module Kotlin qui partage l'ID d'application et la clé de signature du téléphone, et les /capgo/* chemins Data Layer entre les deux. Réglez d'abord la capability et la signature, puis le code sera court.