LoginSignup
4
1

【Android】Glanceでウィジェット開発〜状態管理編〜

Last updated at Posted at 2023-12-09

本記事は以下の導入編の続きです

  1. 導入編
  2. 状態管理編 ← HERE
  3. 非同期処理編

Glanceウィジェットの状態管理

ウィジェットは通常のアプリとは異なる仕組みで描画・状態管理されるため、Jetpack Compose + ViewModelとはだいぶ要領が変わります。

最初に Glance での状態管理の基本を確認してから以下2種類のケースを紹介します

  • 定期的に更新する
  • ユーザー操作をトリガーに更新する

本記事で作成するアプリ・ウィジェットの全ソースコードをGitHubで公開しています

状態の参照

Glance では簡単に Preferences DataStore を利用して状態を保持&参照できます。

CounterWidget.kt
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型を使うのが無難です。

GlanceAppWidget.kt
    // デフォルトでは GlanceStateDefinition<Preferences> が設定されており、
    // ディスクへの永続化処理が実装済み
    open val stateDefinition: GlanceStateDefinition<*>? = PreferencesGlanceStateDefinition

状態の更新

状態を更新してウィジェットに反映する関数を用意します。

  1. updateAppWidgetStateで状態を更新
  2. updateを呼び出して描画させる
CounterWidget.kt
    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で登録したGlanceAppWidgetReceiveronUpdate() が定期的に呼び出されます。

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: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で適宜変換が必要です。(内部実装はIntをラップしているだけっぽい?)

安全なコルーチンの起動

onUpdateBroadcastReceiver.onReceiveから呼び出されます

  • BroadcastReceiverの生存期間は原則onReceiveの呼び出し中のみ
  • onReceiveはMainスレッドで処理される

したがってonUpdateでの長時間のブロックはNGです。サンプルコードではコルーチンを起動してMainセーフですが、BroadcastReceiverのライフサイクルを無視しているので注意です。一応 goAsync()を利用すると制限を回避できますが、なんとonUpdate()の内部実装で呼び出しているため2度目以降はnullを返してクラッシュします... 安全を期すならServiceWorkerに処理を投げるのが無難です。

参考: Android developer ブロードキャストの概要

ユーザー操作でウィジェットを更新する

次にタップで操作可能なカウンターウィジェットにします

tap_glance.gif

コールバックの登録

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,
        ),
    )
}    
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1