Zum Inhalt springen

Erstellen einer Wear OS-App

GitHub

Diese Anleitung zeigt dir, wie du eine Wear OS Companion-App erstellst, die über dieselben Data Layer-Pfade mit deiner Capacitor Phone-App kommuniziert wie CapgoWatchPlugin auf Android (play-services-wearable).

Before you begin, ensure you have:

  • Android Studio (neueste stabile Version)
  • Ein bestehendes Capacitor Android-Projekt (npx cap add android if needed)
  • Eine physische Wear OS Uhr, die mit dem Phone gekoppelt ist (empfohlen für End-to-End-Tests)
  • Derselbe Signing Key für die Phone-App und die Wear OS Modul Release-Builds

Das Plugin auf dem Smartphone (CapgoWatchPlugin.java) nutzt diese Data-Layer-Routen:

RouteTypZweck
/capgo/messageNachrichtEinweg-Nachrichten (vom Phone zur Watch und umgekehrt)
/capgo/message/withreplyNachrichtWatch-zu-Phone-Nachrichten mit erwarteter JS-Antwort über replyToMessage
/capgo/reply/{callbackId}NachrichtPhone-Antwort-Payload zurück an die Watch
/capgo/contextDataItemAktueller App-Kontext (payload String enthält JSON)
/capgo/userinfo/{uuid}DataItemWartende Benutzerinfo (payload String enthält JSON)

Das Telefon erkennt deine Watch-App mit der capgo_watch Capability (WATCH_APP_CAPABILITY im Plugin).

Nach diesem Guide sieht dein Android-Verzeichnis so aus:

  • Verzeichnisandroid/
    • Verzeichnisapp/ (Phone-Modul, Capacitor Host)
      • …
    • Verzeichniswear/ (neues Wear OS Modul)
      • Verzeichnissrc/main/
        • AndroidManifest.xml
        • java/…/WearMainActivity.kt
        • java/…/CapgoWearBridge.kt
        • res/values/wear.xml
    • settings.gradle
  1. Öffne den android Ordner in Android Studio (nicht das Repo-Root).

  2. Wähle Datei → Neu → Neues Modul…

  3. Wähle Wear OS → Leere Wear-App erstellen (oder Wear OS App mit Compose, falls gewünscht).

  4. Konfiguriere das Modul:

    • Modulname: wear (oder dein bevorzugter Name)
    • Package-Name: sollte idealerweise dem Namespace deiner Phone-App entsprechen
    • Mindestmaß an SDK: passend zu deinem Projekt (Plugin minSdk ist 24)
  5. Klicken Fertigstellen und lass den Gradle-Sync durchlaufen.

Das Capacitor Plugin hängt bereits von com.google.android.gms:play-services-wearable:18.2.0ab. Deine wear Modul benötigt das gleiche API.

Unter wear/build.gradle (oder wear/build.gradle.kts), add:

dependencies {
implementation "com.google.android.gms:play-services-wearable:18.2.0"
// Your UI dependencies (Compose, etc.)
}

Gradle nach der Bearbeitung synchronisieren.

Stelle sicher, settings.gradle enthält das wear-Modul:

include ':app'
include ':wear'

Ausführen npx cap sync android aus deinem Capacitor Projekt, wenn du native Module änderst, und führe dann einen Rebuild in Android Studio durch.

Erstellen 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>

Dadurch kann das Phone-Plugin festlegen, isWatchAppInstalled wenn es die Capability abfragt capgo_watch.

Verwenden Sie dieselbe applicationId in der Phone-App und im Wear-Modul. Setzen Sie diese in wear/build.gradle (und gleichen Sie sie in android/app/build.gradle):

android {
defaultConfig {
applicationId "com.example.myapp" // must match android/app
}
}

Beispiel 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>

Erstellen CapgoWearBridge.kt in deinem Wear-Package. Es spiegelt die Pfade in CapgoWatchPlugin.java.

Das Phone-Plugin weist eine opaque callbackId für jedes /capgo/message/withreply und sendet die Antwort über /capgo/reply/{callbackId}. Die Watch sieht diese ID niemals im Voraus. Nutze für parallele Requests einen requestId in deinem JSON-Payload, gib es vom Phone aus mit replyToMessage, und triggere die Watch-Handler über dieses Feld.

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

Verbinde die Bridge in 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()
}
}

Nutzt applicationContext beim Erstellen von Clients, damit Listener Konfigurationsänderungen überstehen.

Installiere und synchronisiere das Plugin in der Phone-App (wie bei 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() },
});
}

Rufe setupWatchListeners() frühzeitig beim App-Bootstrap (zum Beispiel nach platform.ready()).

  1. Wähle die Smartphone app Run-Konfiguration, Installation auf einem physischen Android-Smartphone.

  2. Wähle die wear Run-Konfiguration, Installation auf der gekoppelten Watch (oder einem Watch-Emulator, der mit dem Phone-Emulator verbunden ist).

  3. Bestätigen getInfo() auf dem Smartphone:

    • isSupported: true wenn Google Play Services und Wearable API verfügbar sind
    • isReachable: true wenn mindestens ein Node verbunden ist
    • isWatchAppInstalled: true wenn die Watch das Signal sendet capgo_watch
  4. Senden CapgoWatch.sendMessage vom Phone aus und verifizieren bridge.onMessage auf der Watch.

  5. Von der Watch aus senden mit sendMessageWithReply und bestätigen messageReceivedWithReply plus replyToMessage wird geliefert auf /capgo/reply/{callbackId}.

  6. Test updateApplicationContext und transferUserInfo in beide Richtungen über die oben genannten DataItem-Pfade.

isSupported: false auf einem echten Smartphone

  • Google Play Services fehlen oder sind veraltet
  • Wearable API auf dem Gerät nicht verfügbar

isReachable: false aber die Uhr ist gekoppelt

  • Bluetooth deaktiviert oder Wear-App wurde nicht kürzlich gestartet
  • Unterschiedlich applicationId oder Signing-Keys zwischen den Modulen
  • Kein Wear OS Node aktuell mit Google Play Services verbunden

isWatchAppInstalled: false

  • Wear Companion APK nicht auf der Uhr installiert
  • Fehlt capgo_watch Eintrag in wear.xml
  • Capability nicht synchronisiert: Wear APK nach Änderung der Capabilities neu installieren

Nachrichten kommen nie an

  • Pfad muss exakt übereinstimmen (/capgo/message, kein benutzerdefinierter Präfix)
  • Payload muss JSON-Text in UTF-8 sein (das Plugin nutzt JSONObject / JSObject Strings)

Antwort wird nie an die Watch zurückgegeben

  • Das Phone muss aufrufen replyToMessage mit dem callbackId von messageReceivedWithReply
  • Dasselbe einbinden requestId im Reply-JSON enthalten, falls die Watch parallele Requests korreliert
  • Ausstehende Antworten laufen nach 5 Minuten ab auf Android (Plugin-Konstante PENDING_REPLY_TTL_MS)

Wenn du es nutzt Erstellen einer Wear OS-App Um die Arbeit an nativen Plugins zu planen, verbinde sie mit Verwendung von @capgo/capacitor-watch für die native Funktion in „Verwendung von @capgo/capacitor-watch“, Capgo Plugin-Verzeichnis für den Produkt-Workflow im Capgo Plugin-Verzeichnis, Capacitor Plugins von Capgo für das Implementierungsdetail in Capacitor Plugins von Capgo, Plugins hinzufügen oder aktualisieren für das Implementierungsdetail beim Hinzufügen oder Aktualisieren von Plugins, und Ionic Enterprise Plugin-Alternativen für den Produkt-Workflow bei den Ionic Enterprise Plugin-Alternativen.