【Jetpack Compose】WearOSで簡単なアプリを作ってみる

この記事はand factory.inc Advent Calendar 2022 20日目の記事です。
昨日は @yuu__uuki さんの [【SwiftUI】カスタムTabViewを実装してみた](https://qiita.com/yuu__uuki/items/8ddbca8aa833763e62e9) でした。

# はじめに

Google製WearOSであるところのPixelWatchが今年10月に発売しました :tada: 
私も早々に買ってちょくちょく弄ってみたので、その経験を元に、
JetpackComposeでごく簡単なアプリを作る手順をまとめてみます。

## 対象のサーフェスについて
前提として、WearOSにはモバイルよりも多彩なサーフェスが用意されています。

![principles_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/114cee26-af0d-8824-df64-e4d267088693.png)
[Wear OS 開発の原則 | Android Developer](https://developer.android.com/training/wearables/principles?authuser=1&hl=ja) より


一般的なモバイルアプリのような、「アプリ一覧から選択し、Activityから開始するアプリ」は、上図では `Overlays` に該当します。
今回はごく簡単なアプリということで、この `Overlays` を作成することを目的とします。

## 環境
開発にはAndroidStudioを使用します。
記事中で使用しているバージョンはFlamingo Canary8です。
WearOS実機は私の手元にはPixelWatchしか無いので、他の実機の場合の紹介はできません。
ご了承ください。

# 端末の準備
PixelWatchがお手元にある場合、通常のAndroid端末と同じ用に、設定→開発者オプションからのADB接続が可能です。
ワイヤレスデバッグのペアリングが楽かと思います。

![adbeem-20221219220207.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/c91c23af-7bc4-bc9f-08ef-09897a7b3624.gif)

実機がお手元ない場合はエミュレータを使用して下さい。
PixelWatchに限らないWearOSの物理ボタンの動作も可能なので、こちらも積極的に活用すると良いと思います。

![スクリーンショット 2022-12-19 22.03.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/bd49acb2-b142-f5d2-92aa-dbebab37b54f.png)

# プロジェクトの設定
AndroidStudioのNewProjectから `WearOS` を選択すれば、必要な設定が済んだプロジェクトが出来上がります。
実装時点では以下のように3つの選択肢がありましたが、この記事の目的通り単純な画面を作成するだけでしたら、
3つ目の「Basic Wear App Without Associated Tile...」（長すぎて表示しきれてません…）を選択することをおすすめします。
前述したTileなどの特殊なサーフェスは存在しない、Activityだけのテンプレートが出来上がります。

![スクリーンショット 2022-12-18 18.04.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/23fc7efb-3dcc-704c-3b4f-adf8cb206b5c.png)

このテンプレートでは、 `AndroidManifest.xml` に以下の文が追加されます。

``` xml
    <uses-feature android:name="android.hardware.type.watch" />

    <application>
        <!-- 重要な箇所以外は省略しています -->
        <meta-data
            android:name="com.google.android.wearable.standalone"
            android:value="true" />
```

> `<uses-feature android:name="android.hardware.type.watch" />` 

Watch向けアプリとして必須なのはこれだけです。
これによってこのアプリがWatch向けであることが明確になります。

> `<meta-data android:name="com.google.android.wearable.standalone"...` 

これは、このアプリがスマートフォンとの連携等を必要としない、Watchだけで完結するスタンドアロンアプリであることを明示する設定です。
これを `false` にしてもアプリのWatchへのインストールは可能ですが、必要としない限りこのままで問題有りません。

より詳しい事は以下のドキュメントをご参照下さい。

- [ウェアラブル アプリを作成して実行する | Android Developers](https://developer.android.com/training/wearables/get-started/creating?hl=ja&authuser=1)
- [スタンドアロンの Wear OS アプリとスタンドアロンではない Wear OS アプリ | Android Developers](https://developer.android.com/training/wearables/apps/standalone-apps?hl=ja)

# Jetpack Composeの画面を作成する
テンプレート作成時点で、JetpackCompose関連のボイラプレート記述は済んでいます。
この時点でアプリをbuildすると、以下のようなColumnに配置したTextだけの画面が実行できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/121670dd-01e0-c2e0-2c5d-bd8635ab689c.png" width="250" />

``` kotlin
@Composable
fun WearApp(greetingName: String) {
    MyAppTheme {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colors.background),
            verticalArrangement = Arrangement.Center
        ) {
            Greeting(greetingName = greetingName)
        }
    }
}

@Composable
fun Greeting(greetingName: String) {
    Text(
        modifier = Modifier.fillMaxWidth(),
        textAlign = TextAlign.Center,
        color = MaterialTheme.colors.primary,
        text = stringResource(R.string.hello_world, greetingName)
    )
}
```

## compose-materialでWearOSらしいUIにする

[androidx.wear.compose:compose-material](https://developer.android.com/jetpack/androidx/releases/wear-compose?hl=ja)には、WearOS向けのUI・各種機能が用意されています。
テンプレートのプロジェクトにデフォルトでimplementされており、これを使うだけで、WearOSの画面に沿ったUIが構築できます。

一例を少し紹介します。

### [Scaffold](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary?hl=ja#Scaffold(androidx.compose.ui.Modifier,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0,kotlin.Function0))
WearOSに特化したSlotを用意したScaffoldです。以下のような処理が簡単にできます。
- 上部の画面に沿った時計表示
- 右の画面に沿ったインジケータ表示
- 画面上下の縁が少し暗くなる処理

### [ScalingLazyColumn](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary?hl=ja#ScalingLazyColumn(androidx.compose.ui.Modifier,androidx.wear.compose.material.ScalingLazyListState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.wear.compose.material.ScalingParams,androidx.wear.compose.material.ScalingLazyListAnchorType,androidx.wear.compose.material.AutoCenteringParams,kotlin.Function1))
画面上下端が少し湾曲するリストを簡単に作成できます。

上記2つを使うと、以下のような画面が出来上がります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/01fc4c01-e9f6-ada9-772b-ab58d59c5c6c.gif" width="250" />

``` kotlin
@Composable
fun ScaffoldAndScalingLazyColumnTest() {
    val listState = rememberScalingLazyListState()
    Scaffold(
        positionIndicator = {
            PositionIndicator(
                scalingLazyListState = listState
            )
        },
        vignette = {
            Vignette(vignettePosition = VignettePosition.TopAndBottom)
        },
        timeText = {
            TimeText()
        }
    ) {
        ScalingLazyColumn(
            state = listState,
            modifier = Modifier.fillMaxWidth()
        ) {
            item {...}
        }
    }
}
```

### [CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary?authuser=1&hl=ja#CircularProgressIndicator(kotlin.Float,androidx.compose.ui.Modifier,kotlin.Float,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp))
画面外周に沿ったインジケータを実装できます。
Scaffoldの時刻表示の部分を避けるようにAngleを設定することにより、自然な見た目を作れます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/64ff1c85-7b75-5cfb-30b9-d82d16af481c.png" width="250" />

``` kotlin
@Composable
fun ScaffoldAndCircularProgressIndicatorTest() {
    Scaffold(timeText = { TimeText() }) {
        CircularProgressIndicator(
            progress = 0.6f,
            modifier = Modifier.fillMaxSize(),
            startAngle = 290f,
            endAngle = 250f,
            strokeWidth = 10.dp
        )
    }
}
```

後はテキストやボタンをJetpackComposeで実装すると、それらしい見た目になると思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/183d0370-0eab-6f34-3c67-ccfde7bcf046.png" width="250" />

``` kotlin
@Composable
fun CircularProgressIndicatorAndBoxTest() {
    Scaffold(timeText = { TimeText() }) {
        Box(modifier = Modifier.fillMaxSize()) {
            CircularProgressIndicator(
              //...省略...
            )
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                  //...省略...
                )
                Button(
                  //...省略...
                }
            }
        }
    }
}
```


# ロジックの実装
UI以外の実装に関しては、通常のAndroidアプリ開発と何ら変わりはありません。
Dagger、Room、DataStore、ViewModel…自由に使用可能です。

ただし、あまり複雑な階層のUIや重い処理を行うロジックを実装することは、WearOSでは推奨されていません。
推奨事項やモバイルアプリとの違いについてのドキュメントも存在しますので、そちらをご参照下さい。
- [Wear OS 開発の原則 | Android Developer](https://developer.android.com/training/wearables/principles?hl=ja&authuser=1)
- [Wear OS とモバイル開発の比較 | Android Developer](https://developer.android.com/training/wearables/wear-v-mobile?hl=ja&authuser=1)

# 試作の紹介
ここまでの内容で、当初の目的のごく単純なアプリは実装可能です。
参考までに、ここまでの内容で私がお試しで作ったアプリを少しご紹介します。

https://github.com/k-shinn/MyWearTimer

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/88b23d38-44e5-7a2a-6b26-29bb4bd16948.gif" width="250" /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/ea5fd937-6a83-0431-bdf7-e219654d43a1.gif" width="250" />

前述したProgressなどを使用した、45秒間でループするタイマーアプリです。
実装ロジックとしては以下のような感じです。
- DaggerHiltで依存管理
- ボタンを押したら開始状態と開始時刻をDataStoreに保存
- タイマーが開始していたら、一定間隔で開始時刻と現在時刻の比較を行い、UIに反映

compose-materialのおかげで、WearOSに馴染んだアプリを簡単に作れたと思います。

# 通知について
簡単な画面実装の内容としては以上ですが、少し通知についても説明します。

## ForegroundService
WearOSでももちろん通知は存在します。通知から実行中のアプリに戻ることも出来ます。
アプリをBackgroundで動かそうとした場合、ForegroundServiceに登録する必要がある点も通常のAndroidアプリと変わりません。
テキストだけの通知を追加して開いてみた例が以下のGif画像です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/f3fe7b42-c5fd-0095-3aeb-476264340d92.gif" width="250" />

## 進行中のアクティビティ
WearOSのファーストビューでは通常の通知は見えません。
その代わり、アイコンだけの通知をファーストビューに表示して、アプリが実行中であることを知らせる 「進行中のアクティビティ（OnGoingActivity）」 という仕組みがWearOSには存在します。
以下のGif画像のうち、画面下端にあるタイマーアイコンが実装してみた例です。
これをタップするとアプリに戻るとができます。（通常の通知と同じように、PendingIntentの実装が必要です）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241152/fdc5c5c2-8e43-099d-47ee-e38ac4a19082.gif" width="250" />

通常の通知の実装に加え、以下のように `OngoingActivity` の実装を加えるだけで表示できます。

``` kotlin
        // 普通の通知でも使うnotificationBuilderとpendingIntent
        val pendingIntent: PendingIntent =
            Intent(packageContext, MainActivity::class.java).let { notificationIntent ->
                // ...省略...
            }
        val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                // ...省略...
            .setSmallIcon(R.drawable.timer)
            .setContentIntent(pendingIntent)

        // ongoingActivity実装、ここを追加
        val status = Status.Builder()
            .addTemplate("timer")
            .build()
        val ongoingActivity =
            OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
                .setStaticIcon(R.drawable.timer)
                .setTouchIntent(pendingIntent)
                .setStatus(status)
                .build()
        ongoingActivity.apply(applicationContext)

        startForeground(NOTIFICATION_ID, notificationBuilder.build())
```

詳しくはコードラボがありますので、そちらをご参照下さい。

- [進行中のアクティビティ](https://developer.android.com/codelabs/ongoing-activity?hl=ja#0)

# まとめ
単純な一画面のアプリを作るだけでしたら、Watch向けのManifest設定を行い、WearOS向けのComposeを使うだけで、概ね実装できます。
是非、お試しでアプリを作って見るところから始めてみて下さい。

# 参考

Wearについてのドキュメントはここから

https://developer.android.com/training/wearables?hl=ja&authuser=1

Composeの実装から入りたい場合はここから

https://developer.android.com/training/wearables/compose?hl=ja&authuser=1