要为您的 Capacitor 应用构建 Wear OS 应用,请给 android/ 项目添加一个原生 Wear OS 模块,为它设置与手机应用相同的应用 ID 和签名密钥,并通过 @capgo/capacitor-watch将两者连接起来,该插件在 Android 上使用 Google 的 Wear OS Data Layer。手机端保留 TypeScript code,手表端运行一个小型 Kotlin 应用,与它交换 JSON。
本指南涵盖模块设置、插件使用的具体 Data Layer 路径、手表端的 Kotlin code、手机端的 TypeScript code、用模拟器测试,以及在 Google Play 上发布。
Capacitor 到 Wear OS 的桥接原理
Wear OS 没有 Capacitor 运行时,因此手表应用是原生的。插件在手机上使用 Data Layer API(play-services-wearable),而您的手表应用使用相同的协议。
| 插件方法 / 事件 | Data Layer 机制 | 路径 |
|---|---|---|
sendMessage (手机到手表) |
MessageClient |
/capgo/message |
messageReceived (手表到手机) |
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} |
消息以原始字节形式携带 UTF-8 JSON。DataItem 则将相同的 JSON 作为字符串存储在 DataMap 键下。 payload。手机通过查找 capability 来检查是否存在手表应用。 capgo_watch.
掌握这些路径就是关键。一旦手表能够读写它们,所有插件方法都能正常工作。
准备工作
- 一个添加了 Android 平台的 Capacitor 8 应用(升级指南 如果你版本较旧)。
- 安装了 Wear OS 系统镜像的最新 Android Studio。
- 装有 Google Play 服务的手机。Data Layer 依赖于此,因此没有 Play 服务的手机会报告
isSupported: false. - 已与 Android 手机配对的 Wear OS 手表或模拟器。与 iPhone 配对的 Wear OS 手表无法连接到你的应用。
步骤 1:安装插件
bun add @capgo/capacitor-watch
bunx cap sync android
同一个包也支持 iOS 上的 Apple Watch,因此跨平台应用只需导入一次。参见 插件文档 查看完整参考资料,以及我们的 Apple Watch 指南 了解 iOS 端相关内容。
第 2 步:在 Android 项目中添加 Wear OS 模块
打开 android/ 在 Android Studio 中(bunx cap open android)并使用 文件 > 新建 > 新建模块 > Wear OS。让 Android Studio 生成模块,可以保证 Gradle 文件与你的 Android Gradle Plugin 和 Kotlin 版本匹配,这比从博客文章里粘贴 build 文件更安全。将模块命名为 wear.
向导会添加 include ':wear' 改为 android/settings.gradle。保留它,并不要修改由 Capacitor 管理的行。
Data Layer 强制执行的两条规则
- 同上
applicationId. 在wear/build.gradle,设置applicationId为与 中的值完全一致android/app/build.gradle。该namespace可以不同,但应用 ID 不能。 - 相同的签名证书。 同一台机器上的 Debug 构建共享 debug keystore,因此这通常在本地有效,但在 release 中会失效。请使用与手机应用相同的 upload key 对 Wear release 构建进行签名。
如果违反了其中任何一条规则,这两个应用将永远无法互相发现,code 中的任何设置都无法修复此问题。
依赖项与 SDK 最低版本
Wear OS 3 及更高版本基于 API 30,因此 minSdk 30 是新建手表应用的实用下限。添加 Data Layer 和协程辅助函数:
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
}
声明能力
创建 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>
该字符串必须是 capgo_watch,因为插件会查询它来计算 isWatchAppInstalled.
将应用标记为手表应用
在 wear/src/main/AndroidManifest.xml,向导会添加 <uses-feature android:name="android.hardware.type.watch" />。决定应用是否能在没有手机的情况下运行。仅显示手机数据的配套应用应声明:
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
步骤 3:在手表上接收数据
一套 WearableListenerService 允许系统在手表 UI 关闭时投递 Data Layer 事件。注册时,针对以下 /capgo 路径设置过滤器:
<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>
一个小型共享 store,供 UI 观察:
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())
}
该服务解析每个路径:
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
}
}
当手表 UI 打开时,直接读取当前上下文,以显示应用关闭期间到达的数据:
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()
}
}
步骤 4:从手表发送数据
手表向所有已连接的手机节点发送。消息用于实时交互,DataItems 用于状态。
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()
}
}
当手机收到 /capgo/userinfo/... 项时,它会发出 userInfoReceived 并删除该条目,防止队列无限增长。
一个极简的 Compose 界面把它们串起来:
@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")}")
}
}
手机生成 callbackId,因此手表无法将回复与特定请求匹配。如果你同时发出多个请求,请在 payload 中带上你自己的 request ID,并在回复中原样带回。
第 5 步:通过你的 Capacitor code 与手表通信
TypeScript 端与你为 Apple Watch 编写的 code 完全一致:
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 上值得注意的细节:
sendMessage当没有连接手表节点时,会以 “No connected Wear OS devices found” 拒绝。isPaired以及isReachable两者都表示“至少有一个 Wear OS 节点已连接”。isWatchAppInstalled表示某个节点正在广播capgo_watch.- 目前 Android 不会将 Reachability 作为事件推送。请在
getInfo()需要获取最新值时进行调用,例如屏幕获得焦点时。 - 手机端的待处理回复会在五分钟后过期。
该用哪个 channel
| 需要 | 使用 | 原因 |
|---|---|---|
| 按钮实时按下,手表应用打开 | sendMessage / /capgo/message |
速度快,但无节点连接时会丢失 |
| 手表应显示的内容 | updateApplicationContext / /capgo/context |
最新值会保留,稍后同步 |
| 绝不能丢失的事件 | transferUserInfo / /capgo/userinfo/{uuid} |
每个事件都是独立的 DataItem |
Data Layer 的限制
- DataItems 大小限制约为 100 KB。消息也应保持简短。建议发送 ID,并从后端获取内容。
- 写入内容相同的 DataItem 不会触发变更事件。如需新事件,请添加时间戳。
- 向手表发送消息会启动你的
WearableListenerService,而不是你的 UI。 - 在手机上,只要 Capacitor 应用进程在运行,插件的监听器就存在。进程终止期间发送的消息不会重放到 JavaScript。DataItems 会保留在 Data Layer 中,因此对于手机稍后需要查看的内容,请使用 context。
- Payload 必须是 JSON 对象。顶层数组和非 JSON 值无法解析。
使用模拟器和真机进行测试
- 在 Device Manager 中创建一个带有 Google Play 系统镜像的手机模拟器和一个 Wear OS 模拟器。
- 使用 Android Studio 的 Wear OS 配对助手将它们配对。它会在手机模拟器上安装配套应用。
- 在手表模拟器上运行
wear配置,然后在手机上运行你的 Capacitor 应用,并bunx cap run android. - 调用
CapgoWatch.getInfo()来自手机。isWatchAppInstalled: true确认能力与包匹配。
对于实体手表,请在手表上启用开发者选项和 Wi-Fi ADB 调试,然后用以下方式连接: adb connect <watch-ip>:<port>.
故障排除
isSupported: false. 手机上缺少 Google Play services 或 Wear OS API。在没有 Play 镜像的模拟器上很常见。
isWatchAppInstalled: false 在手表已连接时。 本指南中介绍的 capgo_watch capability 缺失或拼写错误,或者该手表上未安装 wear app。
消息始终无法到达手表。 不同的 applicationId 或不同的签名密钥。请对比 ./gradlew :app:signingReport 以及 ./gradlew :wear:signingReport.
Context 更新不再到达。 你在重复写入相同的 payload。请添加一个会变化的字段。
Release build 在 debug 下正常,但在 Play 上不行。 如果使用 Play App Signing,手机和手表的 artifacts 必须由同一个 Play Console 应用中的同一 app signing key 签名。
R8 会移除你的 service。 如果使用自定义 shrinking 规则,请保留仅被 manifest 引用的类。
在 Google Play 上发布
Wear OS 应用与手机应用共享包名,因此会发布在同一个 Play Console 应用下。在 Play Console 中添加 Wear OS 设备形态,将 wear bundle 上传到 Wear OS 发布轨道,并添加手表截图。Google 会依据 Wear OS 质量指南审核 Wear OS 应用,因此请检查圆形屏幕布局和触摸目标尺寸。
更新方面, Capgo 实时更新 可以将修复推送到手机应用的 web 层,无需通过应用商店发布,这能覆盖 TypeScript bridge code 中的 bug。手表上的 Kotlin 更改需通过 Google Play 发布。为 JSON payloads 设置版本号,让较新的手机 bundle 与较旧的手表 build 保持兼容。如果你在云端构建手机应用, Capgo 构建 负责 Capacitor Android 构建与签名,你还可以使用以下命令生成匹配的 keystore: Android keystore 生成器.
总结
为 Capacitor 应用做 Wear OS 配套应用,主要分三个部分: @capgo/capacitor-watch 手机端一个与手机应用共用 application ID 和签名密钥的 Kotlin 模块,以及 /capgo/* 两者之间的 Data Layer 路径。先把 capability 和签名弄对,code 就很快了。