Capacitor アプリ用の Wear OS アプリを構築するには、ネイティブの Wear OS モジュールを android/ プロジェクトに追加し、スマホアプリと同じアプリケーション ID と署名キーを設定して、両者を @capgo/capacitor-watch、これはAndroid上でGoogleのWear OS Data Layerを使用します。スマートフォン側はTypeScriptの code を保持し、ウォッチ側はそれとJSONをやり取りする小さなKotlinアプリを実行します。
このガイドでは、モジュールのセットアップ、プラグインが使用する正確な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} |
MessageはUTF-8 JSONを生のバイト列として送ります。DataItemsは、同じJSONをDataMapキーの下に文字列として保持します。 payload. スマホ側はcapabilityを検索して、ウォッチアプリの有無を確認します capgo_watch.
これらのパスを把握することが最大のポイントです。ウォッチ側で読み書きができれば、すべてのプラグインメソッドが動作します。
必要なもの
- Androidプラットフォームを追加した Capacitor 8アプリ (アップグレードガイド 最新でない場合)。
- Wear OSのシステムイメージがインストールされた、最新のAndroid Studio。
- Google Play servicesを搭載したスマートフォン。Data Layerはこれに依存しているため、Play servicesがない端末は報告します
isSupported: false. - AndroidスマホとペアリングされたWear OSウォッチまたはエミュレータ。iPhoneとペアリングされたWear OSウォッチからは、アプリにアクセスできません。
ステップ 1: プラグインをインストール
bun add @capgo/capacitor-watch
bunx cap sync android
同じパッケージでiOS上のApple Watchも処理するため、クロスプラットフォームアプリでは1つのインポートで済みます。詳細は プラグインのドキュメント 詳細なリファレンスと、当社の Apple Watch ガイド (iOS側)をご覧ください。
ステップ 2: Android プロジェクトに Wear OS モジュールを追加
開く android/ Android Studio で (bunx cap open android) を使用して File > New > New Module > Wear OS。Android Studio にモジュールを生成させると、Android Gradle Plugin と Kotlin のバージョンに一致する Gradle ファイルが作成されるため、ブログ記事から build ファイルを貼り付けるよりも安全です。モジュール名は wear.
ウィザードが追加します include ':wear' まで android/settings.gradle. そのまま残し、Capacitor が管理する行には手を加えないでください。
Data Layer が強制する2つのルール
- 同じ
applicationId. ではwear/build.gradle、設定するapplicationId内の値と完全に一致するようにandroid/app/build.gradle。namespaceは異なりますが、アプリケーションIDは一致させる必要があります。 - 署名証明書も同じであること。 1台のマシン上でのデバッグビルドはデバッグ用 keystore を共有するため、ローカルでは動作してもリリース時に失敗することがよくあります。Wear OS のリリースビルドには、スマホアプリと同じアップロードキーで署名してください。
どちらかのルールが破られていると、2つのアプリは互いを認識できず、お使いの 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
}
Capability を宣言する
作成します 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>
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/... アイテムを受信すると、emitします 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そのため、ウォッチ側では返信を特定のリクエストに紐付けられません。複数のリクエストを一度に送る場合は、ペイロードに独自のリクエスト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どちらも「少なくとも1つのWear OSノードが接続されている」ことを意味します。isWatchAppInstalledは、ノードがアドバタイズしていることを意味しますcapgo_watch.- 現在、AndroidではReachabilityはイベントとしてプッシュされません。呼び出すのは
getInfo()最新の値が必要なとき、たとえば画面にフォーカスが当たったときです。 - 保留中の返信はスマホ側では5分で期限切れになります。
使用するチャネル
| 要件 | 使用する | 理由 |
|---|---|---|
| Liveボタン押下、ウォッチアプリ起動 | sendMessage / /capgo/message |
高速だが、ノード未接続時はドロップされる |
| ウォッチに表示する内容 | updateApplicationContext / /capgo/context |
最新値は保持され、後で同期される |
| 失ってはいけないイベント | transferUserInfo / /capgo/userinfo/{uuid} |
各イベントは個別のDataItemです |
Data Layer の制限事項
- DataItems は約 100 KB までしか使用できません。メッセージも小さく保ちましょう。ID を送信し、コンテンツはバックエンドから取得してください。
- 同じ内容で DataItem を書き込んでも、変更イベントはトリガーされません。新しいイベントが必要な場合は、タイムスタンプを追加してください。
- ウォッチへのメッセージで起動するのは
WearableListenerServiceUI ではなく、アプリ本体です。 - スマートフォンでは、Capacitor アプリのプロセスが動作している間だけ、プラグインのリスナーが存在します。プロセスが終了している間に送信されたメッセージは JavaScript にリプレイされません。DataItems は Data Layer に残るので、後でスマートフォン側で確認する必要があるものには context を使ってください。
- ペイロードは 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 サービスまたは Wear OS API が見つかりません。Play イメージなしのエミュレーターではよくあります。
isWatchAppInstalled: false ウォッチが接続されている間に。 この capgo_watch capability が欠落または誤記されているか、そのウォッチに wear アプリがインストールされていない可能性があります。
メッセージがウォッチに届きません。 不一致 applicationId または異なる署名キーです。比較してください ./gradlew :app:signingReport と ./gradlew :wear:signingReport.
コンテキストの更新が届かなくなりました。 同じペイロードを繰り返し送信しています。変更されるフィールドを追加してください。
リリースビルドはデバッグでは動作しますが、Play では動作しません。 Play App Signing を使用する場合、スマートフォンとウォッチの両方の成果物は、同じ Play Console アプリ内の同じアプリ署名キーで署名する必要があります。
R8 がサービスを取り除いています。 カスタム shrinking ルールを使用する場合、マニフェストからのみ参照されるクラスを保持してください。
Google Play への公開
Wear OS アプリはパッケージ名を共有するため、スマートフォンアプリと同じ Play Console アプリ配下で公開されます。Play Console に Wear OS フォームファクターを追加し、wear bundle を Wear OS リリーストラックにアップロードして、ウォッチのスクリーンショットを追加してください。Google は Wear OS アプリを Wear OS 品質ガイドラインに基づいて審査するため、丸型スクリーンのレイアウトやタッチターゲットのサイズを確認してください。
更新については、 Capgo Live Updates ストアリリースなしでスマートフォンアプリの Web レイヤーに修正をプッシュでき、TypeScript ブリッジ code のバグをカバーできます。ウォッチ上の Kotlin 変更は Google Play を通ります。新しいスマートフォン bundle が古いウォッチビルドと互換性を保つよう、JSON ペイロードにバージョンを付与してください。スマートフォンアプリをクラウドでビルドする場合、 Capgo のビルド Capacitor の Android ビルドと署名を処理し、対応する keystore は以下で生成できます Android keystore ジェネレーター.
まとめ
Capacitor アプリの Wear OS コンパニオンは、3つの要素で構成されます: @capgo/capacitor-watch スマートフォン側では、アプリ ID と署名キーを共有する Kotlin モジュール、そして /capgo/* その間の Data Layer パスです。まず capability と署名を正しく設定すれば、code までの道のりは短くなります。