Créer une app Wear OS
Copiez un prompt de configuration incluant les étapes d'installation et le guide markdown complet pour ce plugin.
Ce guide vous accompagne dans la création d'une application compagnon Wear OS qui communique avec votre application Capacitor sur téléphone via les mêmes chemins Data Layer que CapgoWatchPlugin sur Android (play-services-wearable).
Prerequisites
Section intitulée « Prérequis »Avant de commencer, assurez-vous d'avoir :
- Android Studio (dernière version stable)
- Un projet Android Capacitor existant (
npx cap add androidsi nécessaire) - Une montre Wear OS physique appairée au téléphone (recommandé pour les tests de bout en bout)
- La même clé de signature pour l'application mobile et les builds de release du module Wear OS
Comment le plugin s'adapte à Wear OS
Section intitulée « Comment le plugin se mappe à Wear OS »Le plugin côté téléphone (CapgoWatchPlugin.java) utilise ces routes Data Layer :
| Route | Type | Objectif |
|---|---|---|
/capgo/message | Message | One-way messages (phone to watch and watch to phone) |
/capgo/message/withreply | Message | Messages de la montre vers le téléphone qui attendent une réponse JS via replyToMessage |
/capgo/reply/{callbackId} | Message | Le payload de réponse du téléphone vers la montre |
/capgo/context | DataItem | Dernier contexte d'application (payload une chaîne JSON) |
/capgo/userinfo/{uuid} | DataItem | Infos utilisateur en attente (payload une chaîne JSON) |
Le téléphone détecte votre application montre avec la capgo_watch capacité (WATCH_APP_CAPABILITY dans le plugin).
Structure du projet
Section intitulée « Structure du projet »Une fois ce guide terminé, votre arborescence Android ressemblera à ceci :
Répertoireandroid/
Répertoireapp/ (module téléphone, hôte Capacitor)
- …
Répertoirewear/ (nouveau module Wear OS)
Répertoiresrc/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
Étape 1 : Ajouter un module Wear OS
Section intitulée « Étape 1 : Ajouter un module Wear OS »-
Ouvrez le
androiddossier dans Android Studio (pas la racine du repo). -
Choisissez Fichier → Nouveau → Nouveau module…
-
Sélectionnez Wear OS → Application Wear vide (ou Application Wear OS avec Compose si vous préférez).
-
Configurez le module :
- Nom du module:
wear(ou le nom de votre choix) - Nom du package: corresponde au namespace de votre app mobile si possible
- Minimum SDK: alignez-le sur votre projet (le plugin
minSdkest 24)
- Nom du module:
-
Cliquez Terminer et laissez la synchronisation Gradle s'effectuer.
Étape 2 : Dépendances Gradle
Section intitulée « Étape 2 : Dépendances Gradle »Le plugin Capacitor dépend déjà de com.google.android.gms:play-services-wearable:18.2.0. Votre wear le module nécessite le même API.
Dans wear/build.gradle (ou wear/build.gradle.kts), ajoutez :
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}Synchronisez Gradle après modification.
Ensure settings.gradle inclut le module wear :
include ':app'include ':wear'Exécutez npx cap sync android de votre projet Capacitor lorsque vous modifiez des modules natifs, puis relancez un build dans Android Studio.
Étape 3 : Déclarer la capgo_watch capability
Étape 3 : Déclarer la capability capgo_watchCréer 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>Cela permet au plugin téléphone de définir isWatchAppInstalled lorsqu'il interroge la capability capgo_watch.
Étape 4 : Manifeste du module Wear et ID d'application
Section intitulée « Étape 4 : Manifeste du module Wear et ID d'application »Utilisez le même applicationId sur l'application téléphone et le module Wear. Définissez-le dans wear/build.gradle (et assurez-vous qu'il correspond dans android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}Exemple 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>Étape 5 : Bridge côté Wear (Kotlin)
Section intitulée « Étape 5 : Bridge côté Wear (Kotlin) »Créer CapgoWearBridge.kt dans votre package wear. Il reflète les chemins de CapgoWatchPlugin.java.
Le plugin téléphone assigne un callbackId pour chaque /capgo/message/withreply et envoie la réponse sur /capgo/reply/{callbackId}. La montre ne voit jamais cet ID à l'avance. Pour les requêtes concurrentes, incluez un requestId dans votre payload JSON, renvoyez-le depuis le téléphone dans replyToMessage, et indexez les handlers de la montre sur ce champ.
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) } } } }
}Branchez le bridge dans 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() }}Utiliser applicationContext lors de la construction des clients pour que les listeners survivent aux changements de configuration.
Étape 6 : Capacitor côté téléphone (TypeScript)
Section intitulée « Étape 6 : Capacitor côté téléphone (TypeScript) »Installez et synchronisez le plugin sur l'application téléphone (identique à 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() }, });}Appelez setupWatchListeners() au début du bootstrap de votre application (par exemple après platform.ready()).
Étape 7 : Build, installation et test
Section intitulée « Étape 7 : Build, installation et test »-
Sélectionnez la téléphone
appconfiguration d'exécution, installez sur un téléphone Android physique. -
Sélectionnez la
wearconfiguration d'exécution, installez sur la montre associée (ou un émulateur de montre lié à l'émulateur du téléphone). -
Confirmez
getInfo()sur le téléphone :isSupported: truelorsque les services Google Play et Wearable API sont disponiblesisReachable: truelorsqu'au moins un nœud est connectéisWatchAppInstalled: truequand la montre diffusecapgo_watch
-
Envoyer
CapgoWatch.sendMessagedepuis le téléphone et vérifierbridge.onMessagesur la montre. -
Envoyer depuis la montre avec
sendMessageWithReplyet confirmermessageReceivedWithReplyplusreplyToMessageest livré sur/capgo/reply/{callbackId}. -
Tester
updateApplicationContextettransferUserInfodans les deux sens en utilisant les chemins DataItem ci-dessus.
Troubleshooting
Section intitulée « Dépannage »isSupported: false sur un téléphone réel
- Services Google Play manquants ou obsolètes
- API Wearable indisponible sur l'appareil
isReachable: false mais la montre est appairée
- Bluetooth désactivé ou l'application montre n'a pas tourné récemment
- Différent
applicationIdou clés de signature différentes entre les modules - Aucun nœud Wear OS actuellement connecté aux services Google Play
isWatchAppInstalled: false
- L'APK compagnon Wear n'est pas installé sur la montre
- Manquant
capgo_watchentrée danswear.xml - Capability non synchronisée : réinstallez l'APK Wear après avoir modifié les capabilities
Les messages n'arrivent jamais
- Le chemin doit correspondre exactement (
/capgo/message, pas un préfixe personnalisé) - Le payload doit être du texte JSON en UTF-8 (le plugin utilise des
JSONObject/JSObjectstrings)
La réponse ne revient jamais vers la montre
- Le téléphone doit appeler
replyToMessageavec lecallbackIddepuismessageReceivedWithReply - Incluez le même
requestIddans le JSON de réponse si la montre corrèle les requêtes simultanées - Les réponses en attente expirent après 5 minutes sur Android (constante du plugin
PENDING_REPLY_TTL_MS)
Étapes suivantes
Section intitulée « Prochaines étapes »- Créer une application watchOS pour Apple Watch et CapgoWatchSDK
- Prise en main pour les API partagés et les notes de plateforme
- Exemples pour les patterns d'app complets
- Source du plugin pour les constantes de chemin côté téléphone
Continuer après « Créer une application Wear OS »
Section intitulée « Continuer après Créer une application Wear OS »Si vous utilisez Créer une app Wear OS pour planifier le travail sur les plugins natifs, connectez-l’à Utilisation de @capgo/capacitor-watch pour la capacité native dans Utilisation de @capgo/capacitor-watch, Capgo Répertoire des plugins pour le workflow produit dans Capgo Plugin Directory, Plugins Capacitor par Capgo pour les détails d'implémentation dans Plugins Capacitor par Capgo, Ajouter ou mettre à jour des plugins pour les détails d'implémentation dans Ajouter ou mettre à jour des plugins, et Alternatives aux plugins Ionic Enterprise pour le workflow produit dans Alternatives aux plugins Ionic Enterprise.