【Android】 Glanceを使ったアプリウィジェット

今回の記事は[レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)の12日目の記事となります。

# はじめに

はじめまして！株式会社レコチョクでAndroid開発を担当しています、入社3年目の杉山です！
アイドルが好きなのですが、今年は色々あったなぁと1年を振り返っているところです。
坂道グループが好きなのですがとてもいいライブがたくさんありました。(最近涙脆いのか泣いてばかりでした笑)

今回は自身の参加しているプロジェクトでアプリウィジェットを実装する際に、導入したGlanceについて記事をまとめたいと思います！

## アプリウィジェットとは
ホーム画面上に表示されるアプリのショートカット機能のことを指します。
アプリを開くことをせず、ホーム画面を見るだけで情報を得ることができる便利さから使っている人も多いかと思います。
みなさんがよく利用しているものでは、時計、天気予報やバッテリー残量などでしょうか。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/e287f0c8-7223-9d9d-f3c5-44a89c41d4d1.png" width="500">

メリットとしては先程も言ったとおり、欲しい情報をひと目で確認できることや、そこから必要な画面に移動してくれることです。
そんなアプリウィジェットの実装方法の一つとして今回はGlanceを紹介します。

## Glanceとは

近年Android開発で使われているJetpack Composeを用いてアプリウィジェットを開発できるようにしたものになります。
Jetpack Composeは2019年に発表されたAndroid用のUIツールキットになります。
Jetpack Composeに関しては今回の記事では詳しいことまで話しませんが、主な利点としては、コード量の削減、直感的な開発、ビルド速度の向上があります。
GlanceはそのJetpack Composeのランタイムをベースに作られたフレームワークになります。
そのため、ホーム画面などで利用できるアプリ ウィジェットをすばやく簡単に構築できるように設計されています。

内部の構造としてはGlanceを用いて作成したComposeをRemoteViewに変換している形になります。
利点としてはJetpack Compose同様、コード量の削減、直感的な開発、ができるようになります。

![image-20221201055533210.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/f03c5eba-f296-e045-7874-4728b054d7c2.png)


## 実際に導入してみた

#### 準備

Glanceを使うにはbuild.gradleに以下の依存関係や設定を追加する必要があります。

```kotlin
// すでにJetpack Composeを使用してる場合
dependencies {
    implementation("androidx.glance:glance-appwidget:1.0.0-alpha03")
}

// 新規にJetpack Composeを使用していない場合、追記する必要があります。
android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.1.0"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}
```

Glanceは専用のComposeを使用するため、普通のComposeの依存関係は不要になります。
(※ Composeを使用するための設定は必要になります。)



#### 各クラスの説明

* GlanceAppWidget

  * ウィジェット本体のクラスになります。レイアウトはこのクラスで設定します。

  ```kotlin
  class MyGlanceAppWidget : GlanceAppWidget() {
      @Composable
      override fun Content() {
        // ここにレイアウトに関する実装をする
      }
  }
  ```

  

* GlanceAppWidgetReceiver

  * ウィジェットの更新を扱うクラスになります。このクラスの` onUpdate`や` onReceive`を設定することで更新処理を変更することができます。

  ```kotlin
  class MyGlanceAppWidgetReceiver : GlanceAppWidgetReceiver() {
  
      override val glanceAppWidget: GlanceAppWidget
          get() = MyGlanceAppWidget()
    	override fun onUpdate() {
        // ここに更新処理を実装する
      }
  }
  ```

  

## 使ってみた

実際に使ってレスポンシブなウィジェットを作成してみました。
ウィジェットのサイズに応じて表示する内容が変わるようになっています。

![作ったウィジェットの動き.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/7cd4f461-d08f-61c1-793b-32df6d12cf0e.gif)

#### Receiver

今回情報更新はないため、Receiverの構成は最小限にしています。

```kotlin
class GlanceAppWidgetReceiverSample : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget
        get() = GlanceAppWidgetSample()
}
```

ウィジェット側からの操作であったり、アプリ側から更新がある場合はこのクラスに実装していきます。

#### AndroidManifest

ウィジェットのReceiverを登録する必要があります。
AndroidManifestにReceiverを登録します。また、meta-dataとしてウィジェットの情報を記載します。

```kotlin
<receiver
    android:name=".GlanceAppWidgetReceiverSample"
    android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/glance_appwidget_sample_meta_data" />
</receiver>
```


#### meta-data
meta-dataとして定義するxmlファイルになります。

```xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:targetCellHeight="2"
    android:targetCellWidth="2"
    android:minWidth="130dp"
    android:minHeight="50dp"
    android:minResizeWidth="130dp"
    android:minResizeHeight="50dp"
    android:maxResizeHeight="130dp"
    android:previewImage="@drawable/henohenomoheji_mark"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/glance_appwidget_sample" />
```

`targetCellWidth`と`targetCellHeight`はAndroid12(SDK31)以上で動作するものになります。
12以上の端末であれば`minWidth`,`minHeight`の代わりに使用されます。
` previewImage`にアプリウィジェット配置時のプレビュー画像を定義することができます。(今回は仮画像を入れています。)
` updatePeriodMillis`は自動更新の際に設定する値で今回は不要であるので0にしています。
` initialLayout` はウィジェットの初回更新がされるまで表示されるレイアウトをになります。ここはまだComposeでの実装はできないようなので、xmlでレイアウトを記述する必要があります。

#### initialLayout

```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">
    <ProgressBar
        android:id="@+id/widget_initial_loading"
        style="?android:attr/progressBarStyle"
        android:layout_width="24dp"
        android:layout_height="24dp" />

    <TextView
        android:id="@+id/widget_initial_loading_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:gravity="center"
        android:paddingVertical="8dp"
        android:text="loading"
        android:textColor="@color/black"
        android:textSize="16sp" />
</LinearLayout>
```

initialLayoutはRemoteViewの制約を受けてしまうので、以下のように使用できる[レイアウト](https://developer.android.com/guide/topics/ui/declaring-layout?hl=ja)が限られます。
そのため今回はLinearLayoutで実装しました。

initialLayoutで使用可能なレイアウト

* FrameLayout
* LinearLayout
* RelativeLayout
* GridLayout


#### GlanceAppWidget
メインとなるウィジェットのレイアウトに関してです。
ウィジェットのサイズに応じて表示するレイアウトを変更するようにできます。
` GlanceAppWidget()`のクラス内に以下のように実装します。

```kotlin
class GlanceAppWidgetSample : GlanceAppWidget() {
   companion object {
        private val LANDSCAPE = DpSize(130.dp, 50.dp)
        private val PORTRAIT = DpSize(130.dp, 130.dp)
        private val ROW = DpSize(210.dp, 50.dp)
        private val MULTI_ROW = DpSize(210.dp, 130.dp)
    }
    override val sizeMode = SizeMode.Responsive(
        setOf(LANDSCAPE, PORTRAIT, ROW, MULTI_ROW)
    )

    @SuppressLint("ResourceType")
    @Composable
    override fun Content() {
        when (LocalSize.current) {
            LANDSCAPE -> SingleResponseLandScape()
            PORTRAIT -> SingleResponsePortrait()
            ROW -> SingleResponseRow()
            MULTI_ROW -> MultiResponseRow()
        }
    }
}
```

` LocalSize.current`によって現在のウィジェットのサイズを取得することができます。

予めサイズ分けしたいものを` SizeMode.Responsive`として` sizeMode`に設定することでサイズに応じたレイアウトをレスポンシブに表示することができます。
動作としてはウィジェットのサイズを変更した際、` LocalSize.current` から現在のウィジェットのサイズを取得し、`when`文から表示するComposeを変更するといった動作になります。
このときに、情報を送ることもできるため、表示する情報を変更することも可能です。 

それぞれ表示するレイアウトは以下のように実装することができます。(今回4種類作成しましたが、そのうちの一つを以下に示します。)

![SingleRow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/1a61bfba-1fea-18c6-f3fa-c06be5969baf.png)

```kotlin
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.text.Text
import androidx.glance.unit.ColorProvider

@Composable
fun SingleResponseRow() {
    Box(
        contentAlignment = Alignment.TopEnd,
        modifier = GlanceModifier
            .padding(5.dp)
            .fillMaxSize()
            .background(Gray)
            .clickable(actionStartActivity<MainActivity>())
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = GlanceModifier
                .fillMaxSize()
                .padding(end = 32.dp)
        ) {
            Image(
                provider = ImageProvider(R.drawable.entertainment_music),
                contentDescription = null,
                modifier = GlanceModifier.size(90.dp)
            )
            Column {
                Text(
                    text = "タイトル名",
                    style = TextStyle(
                        fontWeight = FontWeight.Bold,
                        fontSize = 13.sp,
                        color = ColorProvider(R.color.black)
                    ),
                    maxLines = 2,
                    modifier = GlanceModifier
                        .padding(start = 10.dp)
                )
                Text(
                    text = "アーティスト名",
                    style = TextStyle(
                        fontSize = 11.sp,
                        color = ColorProvider(R.color.black)
                    ),
                    maxLines = 1,
                    modifier = GlanceModifier
                        .padding(start = 10.dp)
                )
            }
        }
        Image(
            ImageProvider(R.drawable.henohenomoheji_mark),
            contentDescription = "App Icon",
            modifier = GlanceModifier
                .size(32.dp, 32.dp)
                .padding(4.dp)
        )
    }
}
```

RowやBoxなどJetpack Composeと同様に実装することができますが、1点注意点があります。
Glanceで使用するComposeは従来のものと異なり、**Glance専用のもの** を使用します。
Box,Row,Text,Image等の同名のComposeがありますが、それぞれJetpack Composeのものとは異なるものになっているため、注意が必要です。
使う際はGlanceのものは、`import androidx.glance.xxx`となります。(上記ソースコードには一部を抜粋して記載してあります。)

主な違いとしては、Modifierが **GlanceModifier** であるという点です。ウィジェット用に変更されているためか、中で設定できることも異なっています。
それ以外は大きな変化はないもののGlance自体が、アルファ版であるため、細かい部分で対応していないものがあったりします。

僕自身も実装する際に、Jetpack Composeのものを使用していてエラーが発生してしまい、躓いたりしました。

GlanceModifierにタップ時の動作を設定することで、アプリを開く事ができます。
```kotlin
@Composable
fun SingleResponseRow() {
    Box(
        contentAlignment = Alignment.TopEnd,
        modifier = GlanceModifier
            .padding(5.dp)
            .fillMaxSize()
            .background(Gray)
            .clickable(actionStartActivity<MainActivity>())
```
`.clickable(actionStartActivity<MainActivity>())` でアプリを開くことができます。
開く画面を別のActivityやIntentにすることで任意の画面を開くことができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298978/6f976d3d-5e49-fed1-1ca4-15ac2474d48a.gif" width="500">

## まとめ
今回はGlanceを用いてアプリのウィジェットを作成したので、紹介しました。
レイアウトを簡潔に書くことができるので、Jetpack Compose同様に今後も業務で使用していきたいと思います。
Receiverを活用することでもっとたくさんの用途を持ったウィジェットを作ることができると思うので今後のアップデートが非常に楽しみです。

最後まで読んでいただきありがとうございました。
明日の [レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)は13日目 S3上のMP4ファイルを切り抜いて高速にレスポンスするWeb APIを作る となります。お楽しみに！

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/8700