🔖

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分間隔であり、それより短い間隔での更新ができないため、AlarmManagerCoroutineWorker を利用する方法を選択しました。

以降、開発の際に注意した点をご紹介します。

設定画面について

ウィジェット設置時や再設定時に表示される画面は、AppWidgetProviderInfoconfigure 属性に指定した 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 Tech Blog

Discussion