Zum Hauptinhalt springen

So baust du eine Wear OS App für deine Capacitor App

Erstelle eine Wear OS App für deine Capacitor App: Füge ein Wear-Modul hinzu, schreibe die Kotlin-Seite für die Watch und synchronisiere die Daten über den Data Layer mit @capgo/capacitor-watch.

Capgo-Banner für ein Tutorial zum Erstellen einer Wear-OS-Companion-App für eine Capacitor-Android-App

Um eine Wear OS-App für deine Capacitor-App zu erstellen, füge ein natives Wear OS-Modul hinzu android/ Projekt hinzu, gib ihm dieselbe Application ID und denselben Signing Key wie der Smartphone-App und verbinde beide mit @capgo/capacitor-watch, das Googles Wear-OS Data Layer unter Android nutzt. Das Telefon behält TypeScript-code, und auf der Uhr läuft eine kleine Kotlin-App, die JSON mit dem Telefon austauscht.

Dieser Leitfaden behandelt die Einrichtung des Moduls, die genauen Data-Layer-Pfade, die das Plugin nutzt, die Kotlin-code auf der Uhr, TypeScript-code auf dem Telefon, das Testen mit Emulatoren und das Veröffentlichen bei Google Play.

So funktioniert die Bridge von Capacitor zu Wear OS

Wear OS verfügt über keine Capacitor-Runtime, daher ist die Watch-App nativ. Das Plugin spricht die Data Layer APIplay-services-wearable) auf dem Telefon, und deine Watch-App spricht dasselbe Protokoll.

Plugin-Methode / Event Data-Layer-Mechanismus Pfad
sendMessage (vom Telefon zur Uhr) MessageClient /capgo/message
messageReceived (von der Uhr zum Telefon) 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}

Messages enthalten UTF-8-JSON als rohe Bytes. DataItems enthalten dasselbe JSON als String unter dem DataMap-Key. payload. Das Telefon prüft auf eine Watch-App, indem es nach der Capability sucht capgo_watch.

Diese Pfade zu kennen, ist der ganze Trick. Sobald die Uhr darauf liest und schreibt, funktionieren alle Plugin-Methoden.

Voraussetzungen

  • Eine Capacitor 8-App mit der hinzugefügten Android-Plattform (Upgrade-Guide falls du zurückliegst).
  • Ein aktueller Android Studio mit installierten Wear OS-Systembildern.
  • Ein Smartphone mit Google Play-Diensten. Die Data Layer hängt davon ab, daher melden Geräte ohne Play-Dienste isSupported: false.
  • Eine Wear OS-Uhr oder ein Emulator, gekoppelt mit einem Android-Smartphone. Wear OS-Uhren, die mit einem iPhone gekoppelt sind, können nicht auf deine App zugreifen.

Schritt 1: Plugin installieren

bun add @capgo/capacitor-watch
bunx cap sync android

Dasselbe Paket deckt Apple Watch unter iOS ab, daher braucht eine plattformübergreifende App nur einen Import. Siehe die Plugin-Docs für die vollständige Referenz und unsere Apple Watch-Anleitung für die iOS-Seite.

Schritt 2: Ein Wear OS-Modul zum Android-Projekt hinzufügen

Öffne android/ in Android Studio (bunx cap open android) und nutze Datei > Neu > Neues Modul > Wear OS. Wenn Android Studio das Modul generiert, erhältst du Gradle-Dateien, die zu deinen Android Gradle Plugin- und Kotlin-Versionen passen – sicherer als eine Build-Datei aus einem Blog-Post zu kopieren. Benenne das Modul wear.

Der Assistent fügt include ':wear' zu android/settings.gradleBehalte es bei und lass die von Capacitor verwalteten Zeilen in Ruhe.

Zwei Regeln, die die Data Layer durchsetzt

  1. Dasselbe applicationId. Unter wear/build.gradle, setzen applicationId auf exakt den Wert in android/app/build.gradle. Die namespace können abweichen, die Application ID aber nicht.
  2. Gleiches Signaturzertifikat. Debug-Builds auf einem Rechner teilen sich den Debug-Keystore, daher funktioniert das lokal meist, bricht aber im Release. Signiere den Wear-Release-Build mit demselben Upload-Key wie die Phone-App.

Wenn eine der beiden Regeln verletzt wird, sehen sich die beiden Apps nie und nichts in deinem code wird das beheben.

Abhängigkeiten und Mindest-SDK

Wear OS 3 und neuer basieren auf API 30, also minSdk 30 ist ein praktischer Mindestwert für eine neue Watch-App. Füge die Data Layer und die Coroutine-Helfer hinzu:

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
}

Capability deklarieren

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

Die Zeichenkette muss capgo_watchsein, da das Plugin genau diese abfragt, um isWatchAppInstalled.

App als Watch-App markieren

Unter wear/src/main/AndroidManifest.xml, fügt der Assistent <uses-feature android:name="android.hardware.type.watch" />hinzu. Entscheide, ob die App ohne das Telefon funktioniert. Eine Companion-App, die nur Telefondaten anzeigt, sollte deklarieren:

<meta-data
    android:name="com.google.android.wearable.standalone"
    android:value="false" />

Schritt 3: Daten auf der Uhr empfangen

Ein WearableListenerService ermöglicht es dem System, Data-Layer-Events zu liefern, auch wenn die Watch-UI geschlossen ist. Registriere es mit Filtern für die /capgo Pfade:

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

Ein kleiner Shared Store, den die UI beobachten kann:

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

Der Service parst jeden Pfad:

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

Wenn die Watch-UI geöffnet wird, lies auch den aktuellen Kontext direkt aus, damit sie Daten anzeigt, die eingegangen sind, während die App geschlossen war:

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

Schritt 4: Daten von der Watch senden

Die Watch sendet an jeden verbundenen Phone-Node. Messages sind für Live-Interaktionen, DataItems für den State.

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

Wenn das Phone ein /capgo/userinfo/... Item empfängt, emittiert es userInfoReceived und löscht das Item, damit die Queue nicht unendlich wächst.

Ein minimaler Compose-Screen bringt alles zusammen:

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

Das Phone generiert das callbackId, damit die Uhr eine Antwort keinem bestimmten Request zuordnen kann. Wenn du mehrere Requests gleichzeitig abschickst, pack deine eigene Request-ID in den Payload und schick sie in der Antwort zurück.

Schritt 5: Mit der Uhr aus deiner Capacitor code sprechen

Die TypeScript-Seite ist derselbe code, den du für die Apple Watch schreiben würdest:

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

Android-spezifische Details, die du kennen solltest:

  • sendMessage wird abgelehnt mit „No connected Wear OS devices found“, wenn kein Wear-OS-Knoten verbunden ist.
  • isPaired und isReachable bedeuten beide, dass mindestens ein Wear-OS-Knoten verbunden ist. isWatchAppInstalled bedeutet, dass ein Knoten sich anmeldet capgo_watch.
  • Auf Android wird die Erreichbarkeit derzeit nicht als Ereignis gesendet. Rufe getInfo() auf, wenn du einen aktuellen Wert benötigst, z. B. wenn der Bildschirm den Fokus erhält.
  • Ausstehende Antworten laufen auf der Smartphone-Seite nach fünf Minuten ab.

Welchen Kanal verwenden

Benötigt Nutzt Warum
Live-Button-Press, Watch-App geöffnet sendMessage / /capgo/message Schnell, aber geht verloren, wenn kein Node verbunden ist
Was die Watch anzeigen soll updateApplicationContext / /capgo/context Letzter Wert wird beibehalten und später synchronisiert
Events, die nicht verloren gehen dürfen transferUserInfo / /capgo/userinfo/{uuid} Jedes Event ist ein eigenes DataItem

Limits des Data Layer

  • DataItems sind auf ca. 100 KB begrenzt. Nachrichten sollten ebenfalls klein bleiben. Sende IDs und lade Inhalte von deinem Backend.
  • Das Schreiben eines DataItems mit identischem Inhalt löst kein Change-Event aus. Füge einen Timestamp hinzu, wenn du ein frisches Event benötigst.
  • Eine Nachricht an die Watch startet deine WearableListenerService, nicht deine UI.
  • Am Telefon existieren die Listener des Plugins, solange der Capacitor-App-Prozess läuft. Nachrichten, die gesendet werden, während er nicht aktiv ist, werden nicht an JavaScript weitergegeben. DataItems verbleiben in der Data Layer, daher Kontext für alles verwenden, was das Telefon später sehen muss.
  • Payloads sind JSON-Objekte. Top-Level-Arrays und nicht-JSON-Werte werden nicht geparst.

Testen mit Emulatoren und Geräten

  1. Erstellen Sie in der Device Manager ein Smartphone-Emulator mit einem Google Play-Systemimage sowie einen Wear OS-Emulator.
  2. Verwende den Wear-OS-Pairing-Assistenten in Android Studio, um sie zu koppeln. Er installiert die Companion-App auf dem Smartphone-Emulator.
  3. Führe die wear Konfiguration auf dem Watch-Emulator aus und starte dann deine Capacitor-App auf dem Smartphone mit bunx cap run android.
  4. Rufe CapgoWatch.getInfo() vom Smartphone aus. isWatchAppInstalled: true bestätigt die Übereinstimmung von Capability und Paket.

Aktiviere auf einer physischen Uhr die Entwickleroptionen und ADB-Debugging über Wi-Fi, verbinde sie dann mit adb connect <watch-ip>:<port>.

Fehlerbehebung

isSupported: false. Google Play-Dienste oder die Wear OS API fehlen auf dem Telefon. Häufig bei Emulatoren ohne Play-Images.

isWatchAppInstalled: false während eine Uhr verbunden ist. Der capgo_watch Die Capability fehlt, ist falsch geschrieben oder die Wear-App ist auf der Uhr nicht installiert.

Nachrichten kommen nie auf der Uhr an. Unterschiedlich applicationId oder unterschiedliche Signing Keys. Vergleiche ./gradlew :app:signingReport und ./gradlew :wear:signingReport.

Context-Updates kommen nicht mehr an. Du schreibst denselben Payload immer wieder. Füge ein sich änderndes Feld hinzu.

Release-Build funktioniert im Debug, aber nicht über Play. Wenn du Play App Signing nutzt, müssen sowohl die Phone- als auch die Watch-Artifacts mit demselben App-Signing-Key in derselben Play Console App signiert sein.

R8 entfernt deinen Service. Behalte Klassen, die nur im Manifest referenziert werden, wenn du eigene Shrinking-Rules verwendest.

Veröffentlichung auf Google Play

Die Wear OS App wird unter derselben Play Console App wie die Phone App veröffentlicht, da sie sich den Package-Namen teilen. Füge den Wear OS Formfaktor in der Play Console hinzu, lade das Wear-Bundle in einen Wear OS Release Track hoch und füge Watch-Screenshots hinzu. Google prüft Wear OS Apps anhand der Wear OS Qualitätsrichtlinien, achte also auf Layouts für runde Displays und die Größen der Touch-Targets.

Für Updates Capgo Live Updates kann Fixes in den Web-Layer der Phone App pushen, ohne Store-Release, was Bugs in deiner TypeScript-Bridge code abdeckt. Kotlin-Änderungen auf der Watch laufen über Google Play. Versioniere deine JSON-Payloads, damit ein neueres Phone-Bundle mit einem älteren Watch-Build kompatibel bleibt. Wenn du die Phone App in der Cloud buildest, Capgo Build übernimmt den Capacitor Android-Build und das Signing, und du kannst einen passenden Keystore mit dem Android Keystore Generator.

Fazit

Ein Wear-OS-Companion für eine Capacitor-App besteht aus drei Bausteinen: @capgo/capacitor-watch auf dem Smartphone, ein Kotlin-Modul, das die App-ID und den Signierungsschlüssel des Telefons teilt, und die /capgo/* Data-Layer-Pfade dazwischen. Erst die Capability und die Signierung richtig konfigurieren, dann ist der code kurz.

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.