4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ゆめみAdvent Calendar 2023

Day 10

【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による変換が必要です。

安全なコルーチンの起動

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

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

したがってonUpdateでの長時間のブロックはNGです。サンプルコードではコルーチンを起動してMainセーフですが、BroadcastReceiverのライフサイクルを無視している点に注意してください。

一般的な対応方法として goAsync()が挙げられますが、super.onUpdate()の内部実装で既に呼び出しているため2度目以降はクラッシュします... 安全を期すなら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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?