创建 Wear OS 应用
复制包含安装步骤和该插件完整 Markdown 指南的配置 prompt。
本指南将带你通过 Wear OS 伴侣应用,利用与 Android 相同的 Data Layer 路径与你的 Capacitor 手机端应用进行通信, CapgoWatchPlugin 例如在 Android 上 (play-services-wearable).
前提条件
“前提条件”章节在开始之前,请确保您已准备好:
- Android Studio (最新稳定版)
- 一个现有的 Capacitor Android 项目 (
npx cap add android(如果需要) - 一台已与手机配对的 Wear OS 真机手表 (推荐用于端到端测试)
- 相同的签名密钥 用于手机 App 和 Wear OS 模块的 release build
插件如何映射到 Wear OS
“插件如何映射到 Wear OS”章节手机端插件 (CapgoWatchPlugin.java) 使用以下 Data Layer 路由:
| 路由 | 类型 | 用途 |
|---|---|---|
/capgo/message | 消息 | 单向消息(手机到手表及手表到手机) |
/capgo/message/withreply | 消息 | 从手表发送到手机的消息,需通过以下方式获取 JS 回复: replyToMessage |
/capgo/reply/{callbackId} | 消息 | 将手机端的回复 Payload 发回手表 |
/capgo/context | DataItem | 最新的应用上下文 (payload 字符串包含 JSON) |
/capgo/userinfo/{uuid} | DataItem | 已排队的用户信息 (payload 字符串包含 JSON) |
手机通过该 capgo_watch 能力(WATCH_APP_CAPABILITY 插件内)检测你的手表应用。
项目结构
“项目结构”章节完成本指南后,你的 Android 目录树如下:
目录android/
目录app/ (手机模块, Capacitor host)
- …
目录wear/ (新增 Wear OS 模块)
目录src/main/
- AndroidManifest.xml
- java/…/WearMainActivity.kt
- java/…/CapgoWearBridge.kt
- res/values/wear.xml
- settings.gradle
步骤 1:添加 Wear OS 模块
标题为“第 1 步:添加 Wear OS 模块”的章节-
打开
androidAndroid Studio 中的文件夹(而非仓库根目录)。 -
选择 文件 → 新建 → 新建模块…
-
选择 Wear OS → 创建空白 Wear App (或 Wear OS 应用 (也可以使用 Compose)。
-
配置模块:
- 模块名称:
wear(或你自定义的名称) - 包名:尽可能与手机端 App 的 namespace 保持一致
- 最低 SDK:需与您的项目保持一致(插件
minSdk为 24)
- 模块名称:
-
点击 完成 并等待 Gradle 同步完成。
步骤 2:配置 Gradle 依赖
标题为 “Step 2: Gradle dependencies” 的章节Capacitor 插件已经依赖于 com.google.android.gms:play-services-wearable:18.2.0。您的 wear module 需要相同的 API。
在 wear/build.gradle (或 wear/build.gradle.kts), 添加:
dependencies { implementation "com.google.android.gms:play-services-wearable:18.2.0" // Your UI dependencies (Compose, etc.)}编辑后请同步 Gradle。
确保 settings.gradle 包含 wear module:
include ':app'include ':wear'运行 npx cap sync android 当你更改原生 module 时,请从你的 Capacitor 项目中同步,然后在 Android Studio 中重新构建。
步骤 3:声明 capgo_watch capability
标题为“步骤 3:声明 capgo_watch capability”创建 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>这允许手机端插件设置 isWatchAppInstalled 当它查询 capability 时 capgo_watch.
步骤 4:Wear 模块 Manifest 与 Application ID
标题为“步骤 4:Wear 模块 manifest 与 application ID”使用相同的 applicationId 在手机 App 和 Wear 模块上。在...中进行设置 wear/build.gradle (并在...中保持一致) android/app/build.gradle):
android { defaultConfig { applicationId "com.example.myapp" // must match android/app }}示例 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>步骤 5:Wear 端 Bridge (Kotlin)
章节标题:“第 5 步:Wear 端桥接 (Kotlin)”创建 CapgoWearBridge.kt 在你的 wear 包中。它镜像了 CapgoWatchPlugin.java.
手机端插件会分配一个不透明的 callbackId 用于每个 /capgo/message/withreply 并将其回复发送至 /capgo/reply/{callbackId}。手表端无法预先获知该 ID。对于并发请求,请包含一个 requestId 在 JSON payload 中,从手机端 echo 该内容,格式为 replyToMessage,并通过该字段在手表端触发 key handlers。
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) } } } }
}在其中搭建桥接 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() }}使用 applicationContext 以确保在构建 client 时,listeners 不会因配置变更而失效。
步骤 6:手机端 Capacitor (TypeScript)
标题为“第 6 步:手机端 Capacitor (TypeScript)”在手机 App 上安装并同步插件(与 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() }, });}调用 setupWatchListeners() 在应用启动初期(例如在 platform.ready()).
步骤 7:Build、安装与测试
“第 7 步:构建、安装和测试”章节-
选择 手机
app运行配置,安装到真实的 Android 手机上。 -
选择
wear运行配置,安装到配对的手表上(或与手机模拟器关联的手表模拟器)。 -
确认
getInfo()在手机上:isSupported: true当 Google Play services 和 Wearable API 可用时isReachable: true当至少有一个节点已连接时isWatchAppInstalled: true当手表进行广播时capgo_watch
-
发送
CapgoWatch.sendMessage从手机发送并验证bridge.onMessage在手表上。 -
从手表发送并使用
sendMessageWithReply并确认messageReceivedWithReply以及replyToMessage实现/capgo/reply/{callbackId}. -
测试
updateApplicationContext以及transferUserInfo使用上述 DataItem 路径进行双向测试。
故障排除
“故障排除”章节isSupported: false 在真实手机上
- Google Play services 缺失或版本过低
- 设备上无法使用 Wearable API
isReachable: false 但手表已配对
- 蓝牙已禁用,或近期未运行手表端 App
- 不一致
applicationId或模块间的签名密钥不匹配 - 当前没有 Wear OS 节点连接到 Google Play services
isWatchAppInstalled: false
- 手表上未安装 Wear companion APK
- 缺失
capgo_watch条目于wear.xml - Capability 未同步:更改 capabilities 后请重新安装 wear APK
消息始终无法送达
- 路径必须完全匹配 (
/capgo/message,而非自定义前缀) - Payload 必须是 UTF-8 编码的 JSON 文本(插件使用
JSONObject/JSObject字符串)
Reply 永远不会返回给手表
- 手机端必须调用
replyToMessage并携带callbackId来自messageReceivedWithReply - 请包含相同的
requestId在 reply JSON 中,如果手表需要关联并发请求 - 待处理的 reply 将在 ___ 后过期 5 分钟 在 Android 上(插件常量
PENDING_REPLY_TTL_MS)
后续步骤
“下一步”章节- 创建 watchOS 应用 适用于 Apple Watch 和 CapgoWatchSDK
- 入门指南 用于共享 API 及平台注意事项
- 示例 用于完整的应用模式
- 插件源码 用于手机端路径常量
继续阅读:创建 Wear OS 应用
标题为“继续阅读:创建 Wear OS 应用”如果你正在使用 创建 Wear OS 应用 要规划原生插件工作,请将其与 使用 @capgo/capacitor-watch 在“使用 @capgo/capacitor-watch”中实现原生能力, Capgo 插件目录 查看 Capgo 插件目录中的产品工作流, Capgo 提供的 Capacitor 插件 查看由 Capgo 提供的 Capacitor 插件中的实现细节, 添加或更新插件 关于“添加或更新插件”中的实现细节,以及 Ionic Enterprise 插件替代方案 关于“Ionic Enterprise 插件替代方案”中的产品工作流。