跳转到正文

创建 Wear OS 应用

GitHub

本指南将带你通过 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/contextDataItem最新的应用上下文 (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. 打开 android Android Studio 中的文件夹(而非仓库根目录)。

  2. 选择 文件 → 新建 → 新建模块…

  3. 选择 Wear OS → 创建空白 Wear App (或 Wear OS 应用 (也可以使用 Compose)。

  4. 配置模块:

    • 模块名称: wear (或你自定义的名称)
    • 包名:尽可能与手机端 App 的 namespace 保持一致
    • 最低 SDK:需与您的项目保持一致(插件 minSdk 为 24)
  5. 点击 完成 并等待 Gradle 同步完成。

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>

创建 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.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)
}
}
}
}
}

在其中搭建桥接 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 步:构建、安装和测试”章节
  1. 选择 手机 app 运行配置,安装到真实的 Android 手机上。

  2. 选择 wear 运行配置,安装到配对的手表上(或与手机模拟器关联的手表模拟器)。

  3. 确认 getInfo() 在手机上:

    • isSupported: true 当 Google Play services 和 Wearable API 可用时
    • isReachable: true 当至少有一个节点已连接时
    • isWatchAppInstalled: true 当手表进行广播时 capgo_watch
  4. 发送 CapgoWatch.sendMessage 从手机发送并验证 bridge.onMessage 在手表上。

  5. 从手表发送并使用 sendMessageWithReply 并确认 messageReceivedWithReply 以及 replyToMessage 实现 /capgo/reply/{callbackId}.

  6. 测试 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)

继续阅读:创建 Wear OS 应用

标题为“继续阅读:创建 Wear OS 应用”

如果你正在使用 创建 Wear OS 应用 要规划原生插件工作,请将其与 使用 @capgo/capacitor-watch 在“使用 @capgo/capacitor-watch”中实现原生能力, Capgo 插件目录 查看 Capgo 插件目录中的产品工作流, Capgo 提供的 Capacitor 插件 查看由 Capgo 提供的 Capacitor 插件中的实现细节, 添加或更新插件 关于“添加或更新插件”中的实现细节,以及 Ionic Enterprise 插件替代方案 关于“Ionic Enterprise 插件替代方案”中的产品工作流。