メインコンテンツへスキップ

Capacitor アプリ向け Wear OS アプリのビルド方法

Capacitor アプリ用の Wear OS アプリを作る:wear モジュールを追加し、Kotlin でウォッチ側を実装、@capgo/capacitor-watch. を使って Data Layer 経由でデータを同期

Capgo バナー: Capacitor Android アプリ用の Wear OS コンパニオンアプリ構築チュートリアル

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つのルール

  1. 同じ applicationId. では wear/build.gradle、設定する applicationId 内の値と完全に一致するように android/app/build.gradle。 namespace は異なりますが、アプリケーションIDは一致させる必要があります。
  2. 署名証明書も同じであること。 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 以外の値はパースされません。

エミュレータと実機でのテスト

  1. Device Manager で、Google Play システムイメージを使用したスマートフォン エミュレータと、Wear OS エミュレータを作成します。
  2. Android Studio の Wear OS ペアリング アシスタントでペアリングします。スマートフォン エミュレータにコンパニオンアプリがインストールされます。
  3. ウォッチのエミュレータで wear ウォッチエミュレータで設定し、その後、Capacitor アプリをスマートフォンで実行します bunx cap run android.
  4. 呼び出す 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 までの道のりは短くなります。

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。