本記事は以下の導入編の続きです
Glanceウィジェットの状態管理
ウィジェットは通常のアプリとは異なる仕組みで描画・状態管理されるため、Jetpack Compose + ViewModelとはだいぶ要領が変わります。
最初に Glance での状態管理の基本を確認してから以下2種類のケースを紹介します
- 定期的に更新する
- ユーザー操作をトリガーに更新する
本記事で作成するアプリ・ウィジェットの全ソースコードをGitHubで公開しています
状態の参照
Glance では簡単に Preferences DataStore を利用して状態を保持&参照できます。
class CounterWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
val pref: Preferences = currentState()
val state = pref[PREF_KEY_COUNT] ?: 0
WidgetTheme {
CounterScreen(
count = state,
)
}
}
}
companion object {
private val PREF_KEY_COUNT = intPreferencesKey("pref_key_count")
}
}
カスタム型の状態を扱う
GlanceAppWidget.stateDefinition
をoverrideすることで Preferences
以外の状態も扱えます。ただしウィジェットの状態を正しく保持するには永続化が必須ですので、デフォルトのPreferences
型を使うのが無難です。
// デフォルトで GlanceStateDefinition<Preferences> が設定されており、
// ディスクへの永続化処理が実装済み
open val stateDefinition: GlanceStateDefinition<*>? = PreferencesGlanceStateDefinition
状態の更新
状態を更新してウィジェットに反映する関数を用意します。
-
updateAppWidgetState
で状態を更新 -
update
を呼び出して描画させる
suspend fun increment(context: Context, glanceId: GlanceId) {
updateAppWidgetState(context, glanceId) {
val current = it[PREF_KEY_COUNT] ?: 0
val next = min(current + 1, 99)
it[PREF_KEY_COUNT] = next
}
update(context, glanceId)
}
定期的にウィジェットを更新する
時間経過で自動的に更新する方法です
追加直後 | 時間経過後 |
---|---|
更新頻度の指定
AppWidgetProviderInfo メタデータのupdatePeriodMillis
に更新間隔をミリ秒単位で指定します。するとAndroidManifestで登録したGlanceAppWidgetReceiver
のonUpdate()
が定期的に呼び出されます。
<?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:updatePeriodMillis="1800000"
android:widgetCategory="home_screen" />
最小の更新間隔
APIリファレンスによれば、30分に1回を超える頻度を指定してもonUpdate
が呼ばれない場合があります。
Note: Updates requested with updatePeriodMillis will not be delivered more than once every 30 minutes.
ウィジェットの更新処理
実際の定期更新ではGlanceAppWidgetReceiver.onUpdate()
が呼ばれるので、あらかじめ用意した状態更新&再描画のメソッドを呼び出します。
class CounterWidgetReceiver : GlanceAppWidgetReceiver() {
// 適当なコルーチン起動方法(仮実装)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override val glanceAppWidget: GlanceAppWidget = CounterWidget()
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
coroutineScope.launch {
val manager = GlanceAppWidgetManager(context)
appWidgetIds.map { appWidgetId ->
val glanceId = manager.getGlanceIdBy(appWidgetId)
launch {
counterWidget.increment(context, glanceId)
}
}.joinAll()
}
}
}
GlanceId
複数のウィジェットを識別するため通常はInt
型のidを利用します。ただし Glance では GlanceId
という別の型を利用するためGlanceAppWidgetManager
による変換が必要です。
安全なコルーチンの起動
onUpdate
はBroadcastReceiver.onReceive
から呼び出されます
-
BroadcastReceiver
の生存期間は原則onReceive
の呼び出し中のみ -
onReceive
はMainスレッドで処理される
したがってonUpdate
での長時間のブロックはNGです。サンプルコードではコルーチンを起動してMainセーフですが、BroadcastReceiver
のライフサイクルを無視している点に注意してください。
一般的な対応方法として goAsync()
が挙げられますが、super.onUpdate()
の内部実装で既に呼び出しているため2度目以降はクラッシュします... 安全を期すならService
やWorker
に処理を投げるのが無難です。
ユーザー操作でウィジェットを更新する
次にタップで操作可能なカウンターウィジェットにします
コールバックの登録
Jetpack Compose 同様にクリックイベントは GlanceModifier
で簡単に指定できます。あとはイベントをコールバック関数で受取り、状態の更新&再描画を走らせるだけです。
@OptIn(ExperimentalGlanceApi::class)
@Composable
fun ExampleIconButton(
onIncrement: () -> Unit,
modifier: GlanceModifier = GlanceModifier,
) {
Image(
provider = ImageProvider(R.drawable.ic_arrow_up),
contentDescription = glanceString(R.string.widget_increment_label),
colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimary),
modifier = modifier.clickable(
key = "increment",
block = onIncrement,
),
)
}