Jetpack Glance を活用した TimeTree 公開カレンダーウィジェットの開発
こんにちは。TimeTree で Android エンジニアをしている笠松(社内では Ring と呼ばれています)です。私事ですが、入社してちょうど1年が経ちました。記念すべきこの日に記事を投稿したいと思います🎉
この記事では、Jetpack Glance を用いた TimeTree の公開カレンダーウィジェットの開発について、その技術的な内容を中心にご紹介したいと思います。
公開カレンダーウィジェットとは?
公開カレンダーは、イベント主催者などがイベント情報を手軽に発信・共有できるカレンダー形式のサービスです。
そして公開カレンダーウィジェットは、推しの画像や公開カレンダーの予定情報がホーム画面に届くウィジェットサービスです。日替り/週替り/月替りのプランで有料購読が可能です。
ウィジェットサイズは、LargeサイズとSmallサイズがあります
現在販売中のウィジェットについては、公開カレンダーウィジェット利用ガイドの「現在ウィジェットを販売している公開カレンダー」セクションをご参照ください。
Jetpack Glance とは
Jetpack Glanceは、Jetpack Compose を使ってアプリウィジェットを開発するためのフレームワークです。通常のアプリ開発と同様に Jetpack Compose の宣言的なUI構築の知識や経験を活かせるため、快適にウィジェットを開発できます。
ただし、Compose のコードは内部で RemoteViews
に変換される点、そしてアプリ本体で使用している Composable 関数をそのまま Glance で再利用することはできない点に注意が必要です。Glance 用に提供されている専用の Composable セットを使用する必要があります。
公開カレンダーウィジェットの概要
Jetpack Glance では、ウィジェットの状態を管理する仕組みが用意されています。デフォルトでは PreferencesGlanceStateDefinition
が使われ、これは DataStore Preferences を利用します。公開カレンダーウィジェットでは、この Preferences に kotlinx.serialization を用いて画像のパスや予定情報などをシリアライズしたJSON文字列として保存しています。Composable 関数内からは currentState<Preferences>()
を呼び出すことで、現在の状態 (Preferences) にアクセスできます。そして、保存されたJSON文字列をデシリアライズして UiState に変換し、ウィジェットの描画に利用します。
ユーザーは設定画面やウィジェットピン留め用のシステムダイアログを通じて、購読プランや表示したい公開カレンダーを選択し、その情報が DataStore に保存されます。
ウィジェットは、WorkerManager
でキューイングされた CoroutineWorker
によって、画像と予定を更新します。この更新時に、ユーザーの購読状態も併せて確認します。画像や予定の更新は、各ウィジェットの購読プラン(日替り/週替り/月替り)や予定の終了タイミングに実行されます。この更新タイミングは AlarmManager
によりスケジュールします。appwidget-provider タグの updatePeriodMillis
属性でも定期更新は可能ですが、最短でも30分間隔であり、それより短い間隔での更新ができないため、AlarmManager
と CoroutineWorker
を利用する方法を選択しました。
以降、開発の際に注意した点をご紹介します。
設定画面について
ウィジェット設置時や再設定時に表示される画面は、AppWidgetProviderInfo
の configure
属性に指定した Activity です。同タグの widgetFeatures
属性に reconfigurable
を設定することで、ウィジェット設置後もユーザーがウィジェットを長押しするなどして設定画面を再表示できるようになります。
widgetFeatures
属性には configuration_optional
という値も指定でき、これを設定するとウィジェット設置直後の設定画面表示をスキップできます。今回のケースでは、購読プランや公開カレンダーが未選択の状態を避けるため、このオプションを使用しませんでした。
画像について
ウィジェットに表示する画像は、アプリ固有のファイルストレージ領域に保存しています。Glance の Composable 関数内で Coil などの画像読み込みライブラリを使って直接画像をダウンロード・表示することも可能ですが、前述の CoroutineWorker
によるバックグラウンドでの定期更新処理との整合性や、UI表示時の読み込みパフォーマンスを考慮し、事前にダウンロードした画像をローカルファイルとして参照する方式を選択しました。
画像はアスペクト比を維持して正方形に表示する必要があるため、LocalSize.current
でウィジェットのサイズを取得し、ファイルから読み込んだ画像に対してクロップおよびリサイズ処理を行います。ウィジェットでは3種類のサイズモード(SizeMode.Single
, SizeMode.Exact
, SizeMode.Responsive
)を選択できますが、公開カレンダーウィジェットでは SizeMode.Responsive
を使用しています。SizeMode.Exact
モードで取得したウィジェットのサイズでは、画像の表示が正しく機能しないことがあったためです。本来、正確にレイアウトを取得できる SizeMode.Exact
を使用することが望ましいですが、ここは回避策として SizeMode.Responsive
を使用します。
RemoteViews
を介してウィジェットに表示する Bitmap
にはサイズ制限があります。そのため、DataStore から画像パスを読み込み、実際にウィジェットに表示する際には、ウィジェットサイズに基づき、安全マージンをとって 1.4倍のサイズにリサイズしてから Bitmap
を読み込むようにしています。これにより、一部の端末で発生していたエラーを回避できました。
ウィジェットピン留め用のシステムダイアログ表示
GlanceAppWidgetManager#requestPinGlanceAppWidget
関数を使用すると、ユーザーがアプリ内から直接ウィジェットをホーム画面に追加(ピン留め)するためのシステム標準ダイアログを表示できます。
この関数の previewState
引数に、実際にウィジェットが配置された際の初期状態を渡すことで、ダイアログ内に表示されるプレビューを実際のウィジェット表示に近づけることができます。例えば、選択された公開カレンダーの最新画像や予定を表示することが可能です。もし previewState
がホームアプリによってサポートされていない場合や、提供されなかった場合は、appwidget-provider タグの previewImage
属性で指定したプレビュー画像がフォールバックとして使用されます。
GlanceAppWidgetManager#requestPinGlanceAppWidget
を呼び出す前には、AppWidgetManager#isRequestPinAppWidgetSupported()
を使って、現在のホームアプリがウィジェットのピン留め機能をサポートしているかを確認する必要があります。GlanceAppWidgetManager
自体には、このサポート状況を確認するための同等の API は提供されていません。
まとめ
本記事では、Jetpack Glance を用いて開発した TimeTree の公開カレンダーウィジェットの概要と開発時に注意した点についてご紹介しました。
この記事が、少しでもこれから Jetpack Glance を使ってウィジェット開発を行う方々の参考になれば幸いです。

TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion