JetpackGlanceを使用してGithubのContributionをWidgetとして表示してみる

# はじめに

岩手県立大学の佐藤佑哉です。最近、岩手県では雪が降り、冬を感じられる季節となってきました。

しかし、寒くてもKotlinへの熱は絶えないため、Jetpack Glanceを用いたWidgetを紹介していきます(!?)

## Jetpack Glanceとは？

Jetpack GlanceはJetpack Composeのruntime上に構築されたフレームワークで、アプリウィジェットを作成できるフレームワークです。

2021年末あたりからalpha版が出ていたのですが、つい3ヶ月前(2023年9月)に1.0.0がStableになりました！

ちなみにDroidKaigi 2023でもJetpack Glanceは紹介されております！

https://speakerdeck.com/mochico/jetpack-glancedehazimeru-material-3nocolor

## 仕組み
Glanceから提供されたComposableを使用すると、Jetpack Compose ランタムを使用して[RemoteView](https://developer.android.com/reference/android/widget/RemoteViews)に変換し、それをアプリウィジェットに表示します。

このことからComposeを有効にする必要があること、RemoteViewの制約の影響を大きく受けていることがわかります。

この[記事](https://android-developers-jp.googleblog.com/2022/01/announcing-jetpack-glance-alpha-for-app.html)が仕組みについて詳しくお話ししています。

https://android-developers-jp.googleblog.com/2022/01/announcing-jetpack-glance-alpha-for-app.html


## 作りたいWidget

今回はGithubのContributionを表示するWidgetを実装していきたいと思います。
iPhoneでのWidgetは下記写真のように表示されているので、
これに似たWidgetをAndroidでも実装していきたいと思います。

~~なぜ、iPhoneのスクショかというと、私がiPhoneをメインで使用しているからです。~~

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989029/4ec2f3f1-3637-d3f0-fe1f-f2bacb1bcdaa.jpeg" width="500">

## 実装
### 依存関係の追加とComposeを有効化
せっかくなので`androidx.glance:glance-material3:1.0.0`も追加します。

```kotlin
buildFeatures {
        compose = true
        //...
}
dependencies {
    implementation "androidx.glance:glance-appwidget:1.0.0"
    implementation "androidx.glance:glance-material3:1.0.0"
    //...
}
```


## Widgetを表示する
ここでは、簡単にWidgetを表示させてみます。
### GlanceAppWidgetクラスでWidgetの画面を作成する。
`GlanceAppWidget`を継承したクラスを作成します。
そして、`provideGlance()`して、`provideContent()`の引数にComposableを渡します。
ここで実装したUIが表示されるような仕組みになっております。

```kotlin
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text

class GlanceSampleWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent { GlanceSampleContent() }
    }

    @Composable
    private fun GlanceSampleContent() {
        GlanceTheme {
            Column(
                modifier = GlanceModifier
                    .fillMaxSize()
                    .padding(16.dp)
                    .appWidgetBackground()
                    .background(GlanceTheme.colors.primaryContainer),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text("Hello Glance!")
            }
        }
    }
}
```

普段の書き方とはだいぶ違うと思います。
普通の`Modifier`とは違い、Glanceでは`GlanceModifier`を使用したり、`Text`や`Column`の参照先は`androidx.glance.*`になっています。

このようにJetpack GlanceではComposeとは異なる
Composableを使用して実装しております。

### GlanceAppWidgetReceiverクラス
次に`GlanceAppWidgetReceiver`を継承したクラスを作成します。ウィジェットを作成・更新する役目を果たすクラスです。

```kotlin
class GlanceSampleWidgetReceiver(override val glanceAppWidget: GlanceAppWidget = GlanceSampleWidget()) :
    GlanceAppWidgetReceiver()
```

### metadataの記入
ウィジェットのメタデータを書きます。
glanceでは、`@layout/glance_default_loading_layout`を用意してくれているため、ローディングを表示させるようにしてあります。

```xml:xml:.xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:updatePeriodMillis="86400000" />
```

### AndroidManifest.xmlにAppWidgetを設定
先ほど実装したAppWidgetクラスをAppWidgetとして表示されるよう記述しておきます。
```xml:AndroidManifest.xml
<receiver
            android:name=".widget.GlanceSampleWidgetReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/glance_sample_widget_info" />
</receiver>
```

この手続きをすむことでAndroidの方からWidgetとして読み込むことができます！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989029/aadb87a0-34f8-0f40-0728-7f4498f796f9.png" width="300">





## Github Contributionを表示させる
ここから記事の本題に入ります👀
### Github Contributionの取得
大まかな実装は省略いたしますが、下記UseCaseからGithubのContributionを取得していることを念頭に置いておきます👀
```kotlin

// Contributionのデータクラス
data class Contribution(
    val date: LocalDate,
    val count: Int,
)

// 過去2ヶ月分のContributionを取得するUseCaseクラス
class GetContributionsForThePastTwoMonthsUseCase(
    private val githubRepository: GithubRepository
) {

    companion object {
        private const val DAYS_IN_PAST_TWO_MONTHS = 62
    }

    operator fun invoke(username: String): Flow<List<Contribution>> = flow {
        // 今日の時間を取得する
        val today = Clock.System.now()

        // 2ヶ月前の時間を取得する
        val from = today.minus(DAYS_IN_PAST_TWO_MONTHS.days)

        // Contributionをemitし、上位に流します。
        githubRepository.getContributions(
            username = username,
            from = from.toString(),
            to = today.toString()
        )
            .catch { throw it }
            .collect(::emit)
    }
}

```

### Contributionを写真のように表示する時の問題点
#### LazyHorizontalGridはサポートされていない
Jetpack GlanceではLazyHorizontalGridはサポートされていないため、Contributionsを理想のWidgetのようにお手軽に表示できません。
#### LazyRowはサポートされていない
Jetpack GlanceではLazyRowは（以下略）。

### この問題点をふまえて
`List<Contribution>`を要素数7の二次元配列に分けて、forEachで表示するという形で実装いたしました。
```kotlin:widget/GithubContributionWidget.kt
@Composable
    private fun GithubContributionsWidgetContent(
        contributions: List<Contribution>
    ) {
        // 1つの要素が1週間分のContributionを表す
        // weeks = [[Contribution, Contribution, ...], [Contribution, Contribution, ...], ...]
        val weeks = remember(contributions) { contributions.chunked(7) }
        GlanceTheme {
            Box(
                modifier = GlanceModifier
                    .fillMaxSize()
                    .appWidgetBackground()
                    .background(GlanceTheme.colors.background),
                contentAlignment = Alignment.Center
            ) {
                Row {
                    weeks.forEach { weekContributions ->
                        Column(GlanceModifier.padding(1.dp)) {
                            weekContributions.forEach { contribution ->
                                Spacer(
                                    modifier = GlanceModifier
                                        .size(12.dp)
                                        .padding(1.dp)
                                        .cornerRadius(4.dp)
                                        .background(getGithubContributionColor(contribution.count))
                                )
                            }
                        }
                    }
                }
            }
        }
    }
```

以下のようにWidgetを実装をすることができました！
表示させることを意識して、パフォーマンスを度外視して実装しているのであまり良い実装とは言えません😅
しかし、iPhoneとはContributionの数が少ないように感じます・・・。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989029/81fe7cf6-979d-9a77-88bc-3e782fa18b37.jpeg" width="500">

## glanceのComposableは子要素を11個以上表示させることができない
現在、Glanceが提供しているComposableは子要素を10個以上持てないという制約があります。
そのため、3ヶ月(93日)分のContributionをゲットして表示させようとしても以下のようになってしまいます。
70日分のContributionは表示されますが、それ以降のContributionは切り捨てられています。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989029/fd52cfd8-d6b4-7d7e-fc6a-ae93ec450726.jpeg" width="500">

:::note warn
Glanceから提供されたRowやColumnなどのComposableは最大10個の子要素をサポートしますが、それ以上の要素は切り捨てられてしまいます。
:::

# まとめ
本記事では、Jetpack Glanceを使用してWidgetの表示の仕方からGithubのContributionを表示させるところまで実装しました。
Jetpack GlanceがStableになって、Widgetについても宣言的に実装できるようになり、ますます開発者体験が上がることが期待できそうです！
今回のケースでは厳しい要件や制約があったために実装が複雑になってしまいましたが、簡単なWidgetに関しては簡単に作れるので、誰かの参考になれば幸いです🙇‍♂️

下記に実装したgithubのサンプルコードを貼っておきます！参考にしていただけますと幸いです🙇‍♂️

https://github.com/dao0203/github-profile-sample


## 参考記事

https://speakerdeck.com/mochico/jetpack-glancedehazimeru-material-3nocolor

https://developer.android.com/reference/android/widget/RemoteViews

https://android-developers-jp.googleblog.com/2022/01/announcing-jetpack-glance-alpha-for-app.html