【Android】Glanceでウィジェット開発〜導入編〜

内容が長くなったので続編に分けました

1. **導入編 ← HERE**
2. [状態管理編](https://qiita.com/Seo-4d696b75/items/235967ed0c4332683f6e)
3. [非同期処理編](https://qiita.com/Seo-4d696b75/items/6c0d46b589d2b0b97868)

------


# 安定版リリース 🎉

https://developer.android.com/jetpack/androidx/releases/glance

Jetpack Composeライクにウィジェットを開発できる androidx.glance のバージョン 1.0.0 が2023年9月に公開されました！

本記事では Glance 入門として簡単なウィジェットを開発します
- セットアップ
- GlanceでUI作成
- Material3のカラーシステムを利用

![hello_glance.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/4c8d8faf-6e16-ba57-e1ff-9043d43c8589.gif)


作成したアプリ・ウィジェットのコードはGitHubで公開しています

https://github.com/Seo-4d696b75/glance-widget-demo/tree/hello-stateless

# Glanceのセットアップ

Jetpack Compose と同様のセットアップ方法に加えて以下の依存も追加します

```gradle:build.gradle
dependencies {
   implementation "androidx.glance:glance-appwidget:1.1.0"
   implementation "androidx.glance:glance-material3:1.1.0"
}
```

# Glance ウィジェット開発の基本

公式ドキュメントに従って進めます

https://developer.android.com/jetpack/compose/glance/create-app-widget?hl=ja

## AppWidgetProviderInfo メタデータを追加する

ウィジェットの基本情報を定義します。基本的にはRemoteViewによるウィジェット開発と同じで、[各種属性の詳細は公式ドキュメントに記載があります。](https://developer.android.com/guide/topics/appwidgets?hl=ja#MetaData)ただし、`android:initialLayout`はXMLファイルが無いのでライブラリ側で事前定義されたものを指定します。


```xml:res/xml/appwidget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/widget_description"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:minWidth="110dp"
    android:minHeight="40dp"
    android:previewImage="@null"
    android:resizeMode="none"
    android:updatePeriodMillis="0"
    android:widgetCategory="home_screen" />
```    

ウィジェットを追加するときのUIを色々カスタマイズできます

- `android:previewImage` ウィジェットのプレビュー画像（指定なしだとアプリアイコン）
- `android:description` ウィジェットの説明文（Android12以降のみ表示されます）
- `android:label` ウィジェットの名前（後述の`AndroidManifest.xml`で指定）

:::note info
添付画像はPixelのemulatorで撮影しました
端末よって見た目が異なる場合もあります
:::

| Android 11 | Android 13 |  
|----|-----|  
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/d6173cdd-9aed-53a5-0fd2-de25797f614c.png">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/41f6d2a6-fc39-0e70-8e58-ac60d38ae505.png">|  

## GlanceAppWidgetの追加

`provideGlance`関数でウィジェットのUIを定義します。ここで Jetpack Compose と同じ要領でUIを作成できるのが Glance 最大の利点です！ 

:::note warn
importする名前空間に注意

Glance でもお馴染みの名前で各種 Composable関数が提供されていますが、import先の名前空間は`androidx.glance.*`です。 `androidx.compose.*`とは異なるので混同に気をつけてください。
:::

```kotlin
class HelloWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            // 以下2点以外はだいたい同じ！
            // - import androidx.compose -> androidx.glance
            // - Modifier -> GlanceModifier
            Box(
                modifier = GlanceModifier.fillMaxSize(),
                contentAlignment = Alignment.Center,
            ) {
                Text("Hello glance!")
            }
        }
    }
}
```

## GlanceAppWidgetReceiverの追加

ウィジェットの追加・更新は`BroadcastRecevier`経由で行われます。Glanceでは専用の`GlanceAppWidgetReceiver`を継承します。

```kotlin
class HelloWidgetReceiver : GlanceAppWidgetReceiver() {
    // 実際に表示するUIを定義したGlanceAppWidgetを返す
    override val glanceAppWidget: GlanceAppWidget = HelloWidget()

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // 今回はstatelessなため画面更新は不要、特別な対応なし
        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
}
```

## AndroidManifestの修正

追加した`GlanceAppWidgetReceiver`をマニフェストで宣言し、Androidシステム側に見えるようにします。

`android:label`属性はウィジェットを追加するとき表示される値です

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <receiver
            android:name=".HelloWidgetReceiver"
            android:exported="true"
            android:label="@string/widget_label">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget_info" />
        </receiver>
    </application>
</manifest>
```

# Material3 カラーシステムを利用する

Jetpack Compose の利点として Materialデザインをコードベースで利用できる点があります。 GlanceでもMaterial3のカラーシステムをKotlinのコードだけで利用できます。（typographyとshapeは未対応）

## GlanceThemeの定義

```kotlin
@Composable
fun WidgetTheme(
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit,
) {
    val colors =
        if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            GlanceTheme.colors
        } else {
            ColorProviders(
                // Jetpack Composeでいつも使う色定義
                // androidx.compose.material3.ColorScheme
                light = LightColorScheme,
                dark = DarkColorScheme,
            )
        }

    GlanceTheme(colors) {
        content()
    }
}
```



## GlanceThemeの利用

1. 定義した`GlanceTheme`でラップする
2. `GlanceTheme`の Composition Local から色リソースにアクセスする

```kotlin
class HelloWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            WidgetTheme {
                HelloScreen()
            }
        }
    }
}
```

```kotlin
@Composable
fun HelloScreen(
    modifier: GlanceModifier = GlanceModifier,
) {
    Box(
        modifier = modifier
            .fillMaxSize()
            .background(GlanceTheme.colors.surface)
            .cornerRadius(8.dp)
            .padding(4.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = glanceString(id = R.string.widget_hello_message),
            style = TextStyle(
                color = GlanceTheme.colors.onSurface,
            )
        )
    }
}
```

:::note warn
`stringResource()`は使えない

以下のように`Context`から直接取得する必要があります

```kotlin
@Composable
fun glanceString(@StringRes id: Int): String {
    return LocalContext.current.getString(id)
}
```
:::

## 動作例

![widget_dark_mode.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/a855b03d-451e-7af7-0dcb-fa4cf457c8f7.gif)



# Tips

ちょっとした工夫や困りポイントの紹介

## モジュール分割
Jetpack Compose と Glance のUI実装を別モジュールにすればimportするとき`androidx.glance.*`と`androidx.compose.*`の混同を避けられます。ただし Material3 のカラー定義は共通化したいのでテーマのモジュールをさらに別途用意しました。

| モジュール | theme | ui | widget |  
|---|:---:|:---:|:---:|  
| 実装内容 | Material3 のカラーと`GlanceTheme`定義 | Jetpack Compose のUI実装 | Glance のウィジェット実装  |   
| `androidx.compose.material3:material3` | ⭕ | ⭕ | |  
| `androidx.glance:glance-appwidget` | ⭕ | | ⭕ |  
| `androidx.glance:glance-material3` | ⭕ | | ⭕ |  


## `GlanceModifier.cornerRadius`

ドキュメントを見ると怪しい記述が...

> Note: Only works on Android S+.


なんと Android S（API 31）未満では角丸になりません。代替案として角丸を定義した drawable に重ねて表示する方法があります。

```xml:res/drawable/widget_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ffffff"/>
    <corners android:radius="8dp" />
</shape>
```

```kotlin
    Box(
        modifier = GlanceModifier.fillMaxSize(),
    ) {
        Image(
            provider = ImageProvider(R.drawable.widget_background),
            contentDescription = null,
            colorFilter = ColorFilter.tint(GlanceTheme.colors.surface),
            modifier = GlanceModifier.fillMaxSize(),
        )
        YourContent()
    }
```