Membuat aplikasi Wear OS
Salin prompt setup yang berisi langkah install dan panduan markdown lengkap untuk plugin ini.
Panduan ini akan memandu Anda membuat aplikasi pendamping Wear OS yang berkomunikasi dengan aplikasi ponsel Capacitor Anda melalui jalur Data Layer yang sama seperti CapgoWatchPlugin pada Android (play-services-wearable).
Prerequisites
Bagian berjudul “Prasyarat”Sebelum memulai, pastikan Anda sudah memiliki:
- Android Studio (versi stable terbaru)
- Proyek Android Capacitor yang sudah ada (
npx cap add android(jika diperlukan) - Smartwatch Wear OS fisik yang terhubung ke ponsel (disarankan untuk end-to-end testing)
- Signing key yang sama untuk aplikasi ponsel dan release build modul Wear OS
Cara plugin memetakan ke Wear OS
Bagian berjudul “Cara plugin memetakan ke Wear OS”Plugin di sisi ponsel (CapgoWatchPlugin.java) menggunakan rute Data Layer berikut:
| Rute | Tipe | Tujuan |
|---|---|---|
/capgo/message | Pesan | Pesan satu arah (ponsel ke jam dan jam ke ponsel) |
/capgo/message/withreply | Pesan | Pesan dari jam ke ponsel yang mengharapkan balasan JS melalui replyToMessage |
/capgo/reply/{callbackId} | Pesan | Payload balasan ponsel kembali ke jam |
/capgo/context | DataItem | Konteks aplikasi terbaru (payload string berisi JSON) |
/capgo/userinfo/{uuid} | DataItem | Info user yang mengantre (payload string berisi JSON) |
Ponsel mendeteksi aplikasi watch Anda dengan capgo_watch capability (WATCH_APP_CAPABILITY di dalam plugin).
Struktur proyek
Bagian berjudul “Struktur proyek”Setelah menyelesaikan panduan ini, struktur Android Anda akan terlihat seperti ini:
Direktoriandroid/
Direktoriapp/ (modul ponsel, host Capacitor)
- …
Direktoriwear/ (modul Wear OS baru)
Direktorisrc/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
Langkah 1: Tambahkan modul Wear OS
Bagian berjudul “Langkah 1: Tambahkan modul Wear OS”-
Buka
androidfolder di Android Studio (bukan root repo). -
Pilih File → New → New Module…
-
Pilih Wear OS → Aplikasi Wear Kosong (atau Aplikasi Wear OS dengan Compose jika Anda mau).
-
Konfigurasi modul:
- Nama modul:
wear(atau nama pilihan Anda) - Nama package: samakan dengan namespace aplikasi ponsel jika memungkinkan
- Minimum SDK: sesuaikan dengan proyek Anda (plugin
minSdkadalah 24)
- Nama modul:
-
Klik Selesai dan biarkan Gradle melakukan sync.
Langkah 2: Dependensi Gradle
Bagian berjudul “Langkah 2: Dependensi Gradle”Plugin Capacitor sudah bergantung pada com.google.android.gms:play-services-wearable:18.2.0. Anda wear modul membutuhkan API yang sama.
Di wear/build.gradle (atau wear/build.gradle.kts), tambahkan:
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}Sync Gradle setelah mengedit.
Pastikan settings.gradle menyertakan modul wear:
include ':app'include ':wear'Jalankan npx cap sync android dari proyek Capacitor Anda saat mengubah modul native, lalu rebuild di Android Studio.
Langkah 3: Deklarasikan capgo_watch capability
Bagian berjudul “Langkah 3: Deklarasikan capability capgo_watch”Buat 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>Ini memungkinkan plugin ponsel mengatur isWatchAppInstalled kapan ia melakukan query capability capgo_watch.
Langkah 4: Manifest modul Wear dan application ID
Bagian berjudul “Langkah 4: Manifest modul Wear dan application ID”Gunakan yang sama applicationId pada aplikasi ponsel dan modul wear. Atur di wear/build.gradle (dan samakan di android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}Contoh 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>Langkah 5: Bridge sisi Wear (Kotlin)
Bagian berjudul “Langkah 5: Bridge sisi Wear (Kotlin)”Buat CapgoWearBridge.kt di dalam package wear Anda. Ini mencerminkan path di CapgoWatchPlugin.java.
Plugin ponsel menetapkan sebuah callbackId untuk setiap /capgo/message/withreply dan mengirimkan balasannya pada /capgo/reply/{callbackId}. Watch tidak akan pernah melihat ID tersebut sebelumnya. Untuk request konkuren, sertakan requestId di payload JSON Anda, echo kembali dari ponsel di replyToMessage, dan handler key di jam tangan berdasarkan field tersebut.
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) } } } }
}Hubungkan bridge di 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() }}Gunakan applicationContext saat membuat client agar listener tetap aktif setelah perubahan konfigurasi.
Langkah 6: Capacitor di sisi ponsel (TypeScript)
Bagian berjudul “Langkah 6: Capacitor di sisi ponsel (TypeScript)”Install dan sync plugin di aplikasi ponsel (sama seperti 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() }, });}Panggil setupWatchListeners() di awal proses bootstrap aplikasi Anda (misalnya setelah platform.ready()).
Langkah 7: Build, install, dan test
Bagian berjudul “Langkah 7: Build, install, dan test”-
Pilih ponsel
appkonfigurasi run, install di ponsel Android fisik. -
Pilih
wearkonfigurasi run, install di jam tangan yang terhubung (atau emulator jam tangan yang terhubung ke emulator ponsel). -
Konfirmasi
getInfo()di ponsel:isSupported: truesaat Google Play services dan Wearable API tersediaisReachable: truesaat setidaknya satu node terhubungisWatchAppInstalled: truesaat jam tangan melakukan advertisingcapgo_watch
-
Kirim
CapgoWatch.sendMessagedari ponsel dan verifikasibridge.onMessagedi jam tangan. -
Kirim dari jam tangan dengan
sendMessageWithReplydan konfirmasimessageReceivedWithReplyditambahreplyToMessageterkirim ke/capgo/reply/{callbackId}. -
Uji
updateApplicationContextdantransferUserInfodi kedua arah menggunakan path DataItem di atas.
Pemecahan Masalah
Bagian berjudul “Troubleshooting”isSupported: false pada ponsel asli
- Google Play services hilang atau kedaluwarsa
- Wearable API tidak tersedia di perangkat
isReachable: false tapi jam sudah terhubung
- Bluetooth mati, atau aplikasi jam belum dijalankan
- Berbeda
applicationIdatau signing key antar modul berbeda - Tidak ada node Wear OS yang terhubung ke Google Play services
isWatchAppInstalled: false
- APK companion Wear belum terinstal di jam
- Hilang
capgo_watchentri diwear.xml - Capability belum sinkron: instal ulang APK wear setelah mengubah capability
Pesan tidak pernah sampai
- Path harus sama persis (
/capgo/message, bukan custom prefix) - Payload harus berupa teks JSON UTF-8 (plugin menggunakan
JSONObject/JSObjectstring)
Reply tidak akan pernah kembali ke watch
- Phone harus memanggil
replyToMessagedengancallbackIddarimessageReceivedWithReply - Sertakan yang sama
requestIddi dalam reply JSON jika watch mengorelasikan request konkuren - Reply yang tertunda akan kedaluwarsa setelah 5 menit pada Android (konstanta plugin
PENDING_REPLY_TTL_MS)
Langkah berikutnya
Bagian berjudul “Langkah selanjutnya”- Membuat aplikasi watchOS untuk Apple Watch dan CapgoWatchSDK
- Memulai penggunaan untuk API bersama dan catatan platform
- Contoh untuk pola aplikasi lengkap
- Sumber plugin untuk konstanta path sisi ponsel
Lanjutkan dari Membuat aplikasi Wear OS
Bagian berjudul “Lanjutkan dari Membuat aplikasi Wear OS”Jika Anda menggunakan Membuat aplikasi Wear OS untuk merencanakan pekerjaan plugin native, hubungkan dengan Menggunakan @capgo/capacitor-watch untuk kapabilitas native di Menggunakan @capgo/capacitor-watch, Direktori Plugin Capgo untuk alur kerja produk di Direktori Plugin Capgo, Plugin Capacitor oleh Capgo untuk detail implementasi pada Capacitor Plugins oleh Capgo Menambahkan atau Memperbarui Plugin untuk detail implementasi di Menambahkan atau Memperbarui Plugin, dan Alternatif Plugin Ionic Enterprise untuk alur kerja produk dalam Alternatif Plugin Ionic Enterprise.