本記事は以下の続編です
ウィジェットで非同期処理を扱う
今回はユーザーがタップすると非同期的に画面を更新するウィジェットを作成します。
作成したアプリ・ウィジェットのソースコードはGitHubで公開しています
Glanceの非同期的な更新処理
ウィジェットを非同期的に更新する関数が以下のように用意されているとします。(GlanceにおけるUIの作成方法・状態管理の実装は前編で説明済みのため割愛します)
class CounterWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
val pref: Preferences = currentState()
val count = pref[PREF_KEY_COUNT] ?: 0
val isLoading = pref[PREF_KEY_IS_LOADING] ?: false
WidgetTheme {
CounterScreen(
isLoading = isLoading,
count = count,
// どうやってコルーチン起動する? CoroutineScopeはどこ?
onIncrement = { /* TODO */ },
onDecrement = { /* TODO */ },
)
}
}
}
suspend fun increment(context: Context, glanceId: GlanceId) {
val pref: Preferences = getAppWidgetState(context, glanceId)
val isLoading = pref[PREF_KEY_IS_LOADING] ?: false
val current = pref[PREF_KEY_COUNT] ?: 0
if (isLoading) return
// ローディング中にUI更新
updateAppWidgetState(context, glanceId) {
it[PREF_KEY_IS_LOADING] = true
}
update(context, glanceId)
// 非同期処理
val next = min(current + 1, 99)
delay(1000L)
// 結果をUI更新
updateAppWidgetState(context, glanceId) {
it[PREF_KEY_COUNT] = next
it[PREF_KEY_IS_LOADING] = false
}
update(context, glanceId)
}
suspend fun decrement(context: Context, glanceId: GlanceId) {
// 省略
}
companion object {
private val PREF_KEY_COUNT = intPreferencesKey("pref_key_count")
private val PREF_KEY_IS_LOADING = booleanPreferencesKey("pref_key_is_loading")
}
}
Workerで非同期処理
状態管理編で少しだけ触れましたが、ウィジェットを定期的に更新する BroadcastReceiver.onReceiver()
では長時間のスレッドブロックはNGです。また普段の Jetpack Compose + ViewModel のように viewModelScope
も使えないので、時間を要する処理は Service
やWorker
の利用が必要です。本記事では One-Shot な非同期処理を手軽に扱えるWorker
を採用します。
セットアップ
dependencies {
implementation "androidx.work:work-runtime-ktx:2.9.1"
}
Workerの実装
便利なことに suspend 関数を実行できる CoroutineWorker
が用意されているので、コルーチンで非同期処理をそのまま書けます。
Intent
に引数を詰め込んで他のActivity
やService
を起動するケースと似ていますが、
- 処理の成功・失敗に応じて
doWork()
はListenableWorker.Result
型を返す - 引数は
WorkerParameters
型で渡される
とお作法が異なるので注意。
class CountWorker constructor(
private val context: Context,
private val params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork() = withContext(Dispatchers.IO) {
try {
val widget = CounterWidget()
val appWidgetIds =
params.inputData.getIntArray(KEY_APP_WIDGET_IDS) ?: throw IllegalArgumentException()
val operation = when (params.inputData.getString(KEY_OPERATION)) {
OPERATION_INCREMENT -> widget::increment
OPERATION_DECREMENT -> widget::decrement
else -> throw IllegalArgumentException()
}
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
appWidgetIds
.map { appWidgetId -> glanceAppWidgetManager.getGlanceIdBy(appWidgetId) }
.map { glanceId -> launch { operation(context, glanceId) } }
.joinAll()
Result.success()
} catch (t: Throwable) {
Result.failure()
}
}
companion object {
const val KEY_APP_WIDGET_IDS = "key_app_widget_ids"
const val KEY_OPERATION = "key_operation"
const val OPERATION_INCREMENT = "operation_increment"
const val OPERATION_DECREMENT = "operation_decrement"
}
}
GlanceId
のシリアライズ
複数のウィジェットを識別するため Glance では GlanceId
という型を利用しますが、型定義の中身は空っぽなマーカーインターフェイスです。そのままではシリアライズできないため、GlanceAppWidgetManager
でInt
型に変換して引数に渡しています。
Workerの起動
Intent
を投げて起動するActivity
やService
とは異なり、WorkManager
にリクエストを追加することで起動します。今回はユーザーイベントに対して即座かつ1回限りで実行されるため、OneTimeWorkRequest
をリクエストする関数を外部に公開しておきます。
companion object {
fun requestIncrement(
context: Context,
appWidgetIds: IntArray,
) {
val request = OneTimeWorkRequestBuilder<CountWorker>()
.setInputData(
workDataOf(
KEY_APP_WIDGET_IDS to appWidgetIds,
KEY_OPERATION to OPERATION_INCREMENT,
)
)
.build()
WorkManager.getInstance(context).enqueue(request)
}
fun requestDecrement(
context: Context,
appWidgetIds: IntArray,
) {
// 省略
}
}
GlanceとWorkerの接続
用意したWorker起動用の関数はコルーチン不要で呼び出せます。
class CounterWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
+ // GlanceId を直接渡せないのでIntに変換
+ val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
provideContent {
val pref: Preferences = currentState()
val state = pref[PREF_KEY_COUNT] ?: 0
val isLoading = pref[PREF_KEY_IS_LOADING] ?: false
WidgetTheme {
CounterScreen(
isLoading = isLoading,
count = state,
- onIncrement = { /* TODO */ },
- onDecrement = { /* TODO */ },
+ onIncrement = {
+ CountWorker.requestIncrement(context, intArrayOf(appWidgetId))
+ },
+ onDecrement = {
+ CountWorker.requestDecrement(context, intArrayOf(appWidgetId))
+ },
)
}
}
}
BroadcastReceiver
経由の定期更新からも呼び出せます
class CounterWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = CounterWidget()
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
+ CountWorker.requestIncrement(context, appWidgetIds)
}
}