Wear OS スマウォでスマホと連携する単語帳アプリをつくってみよう

📝 この記事は、はてなエンジニア Advent Calendar 2023 の 2024 年 1 月 12 日の記事です。

昨日は id:yohfee さんによる「Mackerel DSLを作ろう」でした。冒頭のコードからこれはなんだ! と興味を惹かれて読み進めました。ネタバレは避けますが自分が知らなかった言語の話はどれも興味深いです。ぜひお読みください。


あけましておめでとうございます。せっかく ⌚️ Pixel Watch をもってるので Wear OS のアプリを年末年始の休みを活かしてつくりたいと思い、その悪戦苦闘を紹介しようと思います。2024 は Wear OS の年になるという願いをこめています。

テーマ

せっかくなので、テーマは「スマホと連携できるもの」にしたいとおもいます。

ただ、連携のしかたは知らないので、Wear OS のいろんなアプリみてるとできるだろうという見切り発車 🚞💨でいきます。(失敗したらオチなしで途中終了になるので、公開後逃亡してこの記事は二度と開かないようにしようと思います)

題材

題材は「単語帳アプリ」にしようとおもいます! 理由は自分がほしいので。

使われ方としては、スマホでインターネットしていて知らない単語が出てきたら調べてメモする。ウォッチ側は不定期に単語テストを出題して定着させるというものを想定します。

テーマの連携については、スマホ側にデータを蓄積しつつも、ウォッチ側にもデータ連携する必要があるので、テーマの部分はきっとやることになるのではという目論見もみこんでいます。

作成

さっそくプロジェクトをつくります。スケルトンプロジェクトは Android Studio のテンプレートから簡単に作成できます。

今回のポイントとしては、Pair with Empty Phone app で連携するスマートフォンアプリも作成するようにすることでしょう。

Pair with Empty Phone app にチェックを入れておく

そうすると、mobilewear の 2 つのモジュールがあるプロジェクトが作成されます。ここにはテンプレートのコードが既に入ってるので、そのまま起動できますから、ここから始めていくのがよいでしょう。

mobile と wear のあるプロジェクトが作成される

これでひながたができたので初めていきます。ちなみに wear 側は Jetpack Compose で書かれているのに、mobile 側が Android View で書かれていて謎だなと思いました。

Wear OS 開発を学ぶ

まず、軽く公式のガイドをながめてみます

developer.android.com

Wear OS も基本的にスマートフォン向けの Android アプリ開発と変わるところはなく、Jetpack Compose をはじめとして同じ書き方で書けるのは Android アプリ開発者にとっては学習コストが低くてうれしいところですね。

しかしながら当然微妙に異なる部分や考慮事項もあります:

developer.android.com

通常の Android 開発ガイドと同じように、サンプルアプリがつまった GitHub リポジトリもあります:

github.com

このあたりを大いに参考にしていくことになるでしょう。

Horologist

あと、知っておくとよいのは、上のガイドでもしれっとリンクが貼られてたりするのですが、Horologist というライブラリがあって、補足的な機能が提供されていたりします:

google.github.io

Jetpack Compose でおなじみの Accompanist の Wear OS 版みたいなもので、実際対比になっていることが説明されています:

The name mirrors the Accompanist name, and is also Watch related.

Horologist とは「時計学者 、時計工」という意味ということです。

機能

付け焼き刃の事前知識は仕入れたので、この試みではどういう機能を実装するか考えてみます。

  1. まずスマートフォンアプリは単純にアプリ内 DB (Room) を使い、そこに単語と意味を保存していくことにします
  2. スマートフォン側で単語を追加するたびにウォッチ側で新しい単語を覚えたねという表示を出すようにします
  3. ウォッチ側でアプリを開いたら、スマートフォン側で登録した単語の意味チェックテストを 1 問出題できるようにします

これくらいの機能に絞ります。時間も方法もないので妥当どころか盛りすぎなところでしょう。

スマートフォン側アプリ

まず、スマートフォン側のアプリがどういうものになったか軽く紹介します。

通常の開発とまったく変わらず大したことはしなくて、単純に Room のデータベースで永続化、単語一覧画面と単語追加画面でそれを表示・記録できるようにしました:

 
単語一覧画面 (ホーム) / 単語追加画面

特筆することがないよくあるサンプルアプリです。削除や編集はありません。余裕は皆無です。

ウォッチ側アプリ

いささかざっくりとしたものを用意しました。起動画面は単語テストを試みるボタンのみです:

単語テストボタンがあるのみの起動画面

単語テスト出題画面は、スマートフォン側で保存された単語からランダムに選ばれた単語とその意味を 3 つの選択肢から答えるものです:

単語テスト出題画面

答えた選択肢が正しい場合は 👍 を、間違ってる場合は ❌ をふわっと一瞬表示して結果を出して終わります:

正解 と 不正解 の表示

そして、スマートフォン側で単語を追加したときに連携して表示される、通称がんばってるね画面です:

がんばってるね画面

Fitbit とかでも目標の歩数に達したら音と共にゴール達成の表示が出たりしますが、あれっぽいものをイメージしています。

動きはこんな感じです:

単語テストの動き

単語テストをして正解したらいい感じに表示を出したりしたかった。

単語追加時の連携

追加時もこれだけ覚えたよとがんばってるねと言ってほしかった。

データ連携の作戦

さて、今回のテーマであり肝心要の難しいポイントのデータ連携について考えていきたいと思います。以下を読むとデータの共有や連携の手段がいろいろ用意されていることに注目してください:

developer.android.com

そもそもウォッチ側の通信は常に直接ないしスマートフォンが側にあってインターネットに接続できているとは限らないなど考慮事項が多い印象です。Horologist にはこうした部分をサポートするライブラリもあります:

google.github.io

話題はもどって、いろいろみた結果、自分が思いうかんだ作戦案はこうです:

  1. クラウド上にデータを置いておき、ウォッチがインターネットを経由してデータを取得する
  2. Data Layer API を使用してメッセージを送り、スマートフォン側がメッセージを受け取り データを交換する。RPC.

今回は (2) の RPC 作戦でお互いがやりとりしあってなんとかする作戦を試みたいと思います。ただ、実際のところは (1) の案で Firebase なりクラウド上にデータを置くほうが取り回しやコード共通化の面でよいだろうと思います。

ちなみに上のガイドでは直接インターネット経由で連携することをオススメしているので逸脱しています。理由はガイドを読むと多分に語られています……。

RPC

もし自分で一から作れといわれたら相当厳しいのですが、Wear OS には Data Layer API という仕組みがあります。

developer.android.com

Horologist でそれをベースにした gRPC による通信の手段が用意されているのでそれを使うことにします。プレーンな仕組みでやればいいのですが、せっかくなので使ってみることにします。

google.github.io

これにより、たとえばスマートフォン側は Service で通信を待ち受けて、ウォッチ側のクライアントが要求を行うとそれに沿ったデータを返してあげれば連携ができるという仕組みがおそらく実現できるのではと思いました。

ただ、ガイドがかなりさっぱりでどうやったら導入できるのかまあまあ不明で途方に暮れます。そのため理解を進めるにサンプルコードを読むことになります:

github.com

これでもなお難解なので、まず動かして確認してみてトライアンドエラーをすることからはじめることになるでしょう。

実装

というわけで、実装を行なっていきますが、コードは別に詳しく知りたくないと思うのでサクッとどういうことをしているのか説明することにします。

まず、gRPC でどういうやりとりをするかの Protobuf の定義をスマートフォン・ウォッチ共通のモジュールの shared をつくり、そこにおくことにします:

message WordData {
  uint64 id = 1;
  string value = 2;
  string meaning = 3;
}

message WordCountData {
  uint64 count = 1;
}

message RandomWordRequest {
  uint32 count = 1;
}

message RandomWordResponse {
  repeated WordData words = 1;
}

service WordSyncServerService {
  rpc getRandomWords(RandomWordRequest) returns (RandomWordResponse);
  rpc getWordCount(.google.protobuf.Empty) returns (WordCountData);
}

で、ライブラリ側がこれを基にコードを生成してくれるので、これを使ってサービスを提供します。Jetpack の Proto DataStore のような雰囲気がしてきました。

今回は保存された単語データはスマートフォン側のアプリ内 DB にあるわけなので、スマートフォン側のコードではサービスを公開して単語をランダムで返したり、単語数を返したりする実装を行なってあげるわけです:

class WordSyncServerService(
    private val wordRepository: WordRepository,
) : WordSyncServerServiceGrpcKt.WordSyncServerServiceCoroutineImplBase() {
    override suspend fun getRandomWords(request: WordSyncProtoData.RandomWordRequest): WordSyncProtoData.RandomWordResponse {
        // データを DB から取得して…
        val words = wordRepository.getSomeRandomWords(request.count)
        // レスポンスの型に詰めて返す
        return WordSyncProtoData.RandomWordResponse.newBuilder()
            .addAllWords(words.map { it.toWordData() })
            .build()
    }

    override suspend fun getWordCount(request: Empty): WordCountData {
        val count = wordRepository.getWordCount()
        return WordCountData.newBuilder()
            .setCount(count.toLong())
            .build()
    }
}

実際のところ、ライブラリの導入やシリアライザの登録などのつなぎこみ系周辺コードが存在するわけですが、本質的にはこの 2 つが今回の連携の肝要な部分です。

で、ウォッチ側のコードではそのサービスを普通に呼び出してあげます:

@HiltViewModel
class WordRememberedScreenViewModel @Inject constructor(
    private val wordSyncServerService: WordSyncServerServiceGrpcKt.WordSyncServerServiceCoroutineStub,
) : ViewModel() {
    // ... 略 ...
    fun load() {
        viewModelScope.launch {
            val wordCount = wordSyncServerService.getWordCount(Empty.newBuilder().build()).count
            state = WordRememberedScreenUiState.Loaded(wordCount)
        }
    }
}

これも同様に周辺コードはあるわけですが、実際の呼び出しはこの普通のメソッドの呼び出しだけなわけであります。簡単そうに感じるのですが、セットアップが大変なのでまあまあな仕事になります。

より簡単で透過的に連携してくれるチャンネルが用意されたらハードルが下がりそうだなと思いました。(既に低レイヤー部分を面倒見てくれてるのだから、空手で RPC を用意するよりは楽だとは思います)。

また、単語追加時のがんばってるね画面は Horologist の Data Layer App Helper を使って明示的インテントを飛ばしています:

google.github.io

つまり、利用者側の視点ではあの画面の表示はスマートフォン側からウォッチ側の Activity を起動しているということになりますね。

全体感

図にまとめるとこういうイメージです:

補足

実際に動いているものです:

youtu.be

コードは以下です:

github.com

最後に

こういった Wear OS 特有の開発に触れることができたのは面白い体験でしたが、結構変な実装をしたという自負はあるので、もっと知識やベストプラクティスを知る必要があると思い知らされましたね。

Wear OS のアプリはコード的には通常の Android 開発とほぼ変わらない書きかたができたわけですが、機能や体験面はより取捨選択が求められる印象があり、そういった意味で通常のアプリとは違った部分があります。いかに手首に向けたアプリを作るか工夫を凝らす楽しみや難しさがありそうという感想をもちました。

いろいろありましたが Wear OS アプリもつくっていきましょう。それでは 👋


👉 はてなエンジニア Advent Calendar 2023 、明日は id:kk__777 さんです!