跳过主要内容

如何为您的Capacitor应用创建一个Wear OS应用

为您的Capacitor应用构建一个Wear OS应用:添加一个Wear模块,编写Kotlin的watch侧代码,并使用@capgo/capacitor-watch同步数据。

文章来源

马丁·多纳迪厄

作者

瓦莱里娅

审稿人

乔丹

编辑

为您的Capacitor应用构建一个Wear OS应用

要为您的Capacitor应用构建一个Wear OS应用,请将一个原生Wear OS模块添加到项目中,给它相同的应用ID和签名密钥,连接它们使用 android/ 项目 @capgo/capacitor-watch, which uses Google’s Wear OS Data Layer on Android. The phone keeps its TypeScript code, and the watch runs a small Kotlin app that exchanges JSON with it.

This guide walks through the module setup, the exact Data Layer paths the plugin uses, the Kotlin code on the watch, the TypeScript code on the phone, testing with emulators, and publishing on Google Play.

Wear OS 桥的 Capacitor

Wear OS 没有 Capacitor 运行时,因此手表应用是本机的。插件在手机上使用 Data Layer API(play-services-wearableWear OS 桥的 __CAPGO_KEEP_0__

Wear OS 桥的 Data Layer 机制 Wear OS 桥的路径 (手机到手表)
sendMessage (手表到手机) MessageClient /capgo/message
messageReceived Wear OS 桥的 DataItem MessageClient /capgo/message
messageReceivedWithReply MessageClient /capgo/message/withreply
replyToMessage MessageClient /capgo/reply/{callbackId}
updateApplicationContext / applicationContextReceived DataClient Wear OS 桥的消息 /capgo/context
transferUserInfo / userInfoReceived DataClient Wear OS 桥的消息 /capgo/userinfo/{uuid}

Wear OS 桥的消息将 UTF-8 JSON 作为原始字节传递。DataItems 将相同的 JSON 作为 DataMap 键下的字符串传递。 payload。手机通过查找能力来检查手表应用 capgo_watch.

了解这些路径就是全部的秘诀。 一旦手表读取和写入它们,所有插件方法都会工作

你需要

  • 一个 Capacitor 8 应用程序,带有添加的 Android 平台(升级指南 如果你落后的话)
  • 一个最近的 Android Studio,带有安装的 Wear OS 系统图像
  • 一个有 Google Play 服务的手机。 Data Layer 依赖于它,所以没有 Play 服务的手机会报告 isSupported: false.
  • 一个配对到 Android 手机的 Wear OS 手表或模拟器。 配对到 iPhone 的 Wear OS 手表无法访问你的应用

步骤 1:安装插件

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

上下文:Capgo 页面。角色:UI 标签。位置:关于 .astro 页面。消息键 `about_how_step_label` (关于如何步骤标签) 插件文档 为了查看完整的参考文档,和我们的 Apple Watch 指南 iOS 端的指南。

步骤 2:在 Android 项目中添加 Wear OS 模块

Open android/ 打开bunx cap open android在 Android Studio( )中使用文件 > 新建 > 新建模块 > Wear OS wear.

。让 Android Studio 自动生成模块,会为您生成与 Android Gradle 插件和 Kotlin 版本匹配的 Gradle 文件,这比从博客文章中粘贴构建文件更安全。命名模块 include ':wear' 向 android/settings.gradle保持它,且不动由Capacitor管理的行。

数据层强制执行的两个规则

  1. 相同 applicationId. 在 wear/build.gradle, 设定 applicationId 为准确地等于 android/app/build.gradle. The namespace 相同的签名证书。
  2. 调试版本在一台机器上共享调试密钥,通常在本地工作,然后在发布时会出现问题。将 Wear OS 发布版本签名为与手机应用相同的上传密钥。 如果两个规则之一被破坏,两个应用将无法相互看到,任何在您的 __CAPGO_KEEP_0__ 中的修复都无法解决问题。

依赖项和最低 code

Wear OS 3 和更高版本基于 SDK 30,所以

API minSdk 30 是一个实用的地板为新手表应用。添加数据层和协程助手:

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:在手表上接收数据

A WearableListenerService 一个 /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>

一个可供 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
    }
}

当手表界面打开时,也直接读取当前上下文,以便显示在应用关闭时到达的数据:

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:从手表发送数据

context

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

手表向每个连接的手机节点发送。消息用于实时交互,DataItems用于状态. /capgo/userinfo/... 当手机接收到一个 userInfoReceived 它发射

并删除该项,以防止队列永远增长.

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

一个最小的 Compose 屏幕将其联系起来: callbackId手机生成的

第 5 步:从您的Capacitorcode与手表对话

The TypeScript side is the same code you would write for Apple Watch:

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 当没有手表节点连接时,会拒绝连接并显示“未找到 Wear OS 设备”
  • isPaired 并 isReachable 这两个都意味着“至少有一台 Wear OS 节点连接” isWatchAppInstalled 意味着一个节点广播 capgo_watch.
  • 可达性不会在 Android 上作为事件推送。请调用 getInfo() 当您需要一个最新的值时,例如当屏幕获得焦点时
  • 手机端的等待回复将在五分钟后过期

需要使用哪个频道

需要 使用 为什么
按Live按钮,手表应用打开 sendMessage / /capgo/message 速度快,但如果没有连接的节点会掉落
手表应该显示什么 updateApplicationContext / /capgo/context 最新值会被保存并在后面同步
不能丢失的事件 transferUserInfo / /capgo/userinfo/{uuid} 每个事件都是自己的DataItem

数据层的限制

  • 数据项限制在约100KB。消息也应该保持小巧。发送ID并从您的后端获取内容。
  • 写入相同内容的数据项不会触发变化事件。添加时间戳以获取最新事件。
  • 向手表发送消息会启动 WearableListenerService,不是您的UI。
  • 在手机上,插件的监听器在Capacitor应用程序进程运行时存在。发送到它的死信不会重播到JavaScript。DataItems在数据层中保持不变,因此请使用上下文来处理手机必须看到的任何内容。
  • 载荷是JSON对象。顶级数组和非JSON值将不会解析。

使用模拟器和设备进行测试

  1. 在设备管理器中创建一个带有Google Play系统镜像的手机模拟器和一个Wear OS模拟器。
  2. 使用Android Studio的Wear OS配对助手将它们配对。它在手机模拟器上安装了伴侣应用。
  3. 在 wear 配置上运行watch模拟器,然后在手机上运行Capacitor应用程序。 bunx cap run android.
  4. 从手机 CapgoWatch.getInfo() 确认能力和包匹配。 isWatchAppInstalled: true 对于一个物理手表,启用开发者选项并在手表上通过Wi-Fi启用ADB调试,然后连接

确认能力和包匹配。 adb connect <watch-ip>:<port>.

Troubleshooting

isSupported: false. 手机上缺少Google Play服务或Wear OSAPI

isWatchAppInstalled: false 连接手表时 缺少或拼写错误的功能 capgo_watch 在手表上收不到消息

不同的 或 applicationId 上下文更新停止到达 ./gradlew :app:signingReport and ./gradlew :wear:signingReport.

发布版在调试环境下正常工作,但在Play上却不正常 缺少或拼写错误的功能

在手表上收不到消息 如果您使用Play App Signing,手机和手表的artifact都必须由同一个app签名密钥在同一个Play Console app中签名。

R8会去掉您的服务。 如果您使用自定义缩小规则,请只从清单中引用类。

在Google Play上发布

Wear OS应用程序在同一个Play Console app中发布,因为它们共享包名。请在Play Console中添加Wear OS形式因子,上传wear bundle到Wear OS发布轨道,并添加手表截图。Google会根据Wear OS质量指南评估Wear OS应用程序,因此请检查圆形屏幕布局和触摸目标大小。

对于更新 Capgo实时更新 can push fixes to the phone app’s web layer without a store release, which covers bugs in your TypeScript bridge code. Kotlin changes on the watch go through Google Play. Version your JSON payloads so a newer phone bundle stays compatible with an older watch build. If you build the phone app in the cloud, Capgo 构建 handles the Capacitor Android build and signing, and you can generate a matching keystore with the Android密钥生成器.

总结

一个 Wear OS 配对应用的 Capacitor 应用主要包括三个部分: @capgo/capacitor-watch 在手机上,一个 Kotlin 模块,共享手机的应用 ID 和签名密钥,以及 /capgo/* 数据层路径之间。获取能力和签名正确后,code 就变得简单了。

为Capacitor应用提供即时更新

当web层bug处于活跃状态时,通过Capgo将修复推送给用户,而不是等待几天的应用商店审批。用户在后台接收更新,而原生变化仍然在正常审批路径中。

最新博客文章

Capgo 为您提供了创建真正专业的移动应用所需的最佳见解。