4
3

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 11

【Android】Glanceでウィジェット開発〜非同期処理編〜

Last updated at Posted at 2023-12-11

本記事は以下の続編です

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

ウィジェットで非同期処理を扱う

今回はユーザーがタップすると非同期的に画面を更新するウィジェットを作成します。

async_glance.gif

作成したアプリ・ウィジェットのソースコードはGitHubで公開しています

Glanceの非同期的な更新処理

ウィジェットを非同期的に更新する関数が以下のように用意されているとします。(GlanceにおけるUIの作成方法・状態管理の実装は前編で説明済みのため割愛します)

CounterWidget.kt

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も使えないので、時間を要する処理は ServiceWorkerの利用が必要です。本記事では One-Shot な非同期処理を手軽に扱えるWorkerを採用します。

セットアップ

build.gradle
dependencies {
   implementation "androidx.work:work-runtime-ktx:2.9.1"
}

Workerの実装

便利なことに suspend 関数を実行できる CoroutineWorkerが用意されているので、コルーチンで非同期処理をそのまま書けます。

Intentに引数を詰め込んで他のActivityServiceを起動するケースと似ていますが、

  • 処理の成功・失敗に応じてdoWork()ListenableWorker.Result型を返す
  • 引数はWorkerParameters型で渡される

とお作法が異なるので注意。

CountWorker.kt
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という型を利用しますが、型定義の中身は空っぽなマーカーインターフェイスです。そのままではシリアライズできないため、GlanceAppWidgetManagerInt型に変換して引数に渡しています。

Workerの起動

Intentを投げて起動するActivityServiceとは異なり、WorkManagerにリクエストを追加することで起動します。今回はユーザーイベントに対して即座かつ1回限りで実行されるため、OneTimeWorkRequestをリクエストする関数を外部に公開しておきます。

CountWorker.kt
    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起動用の関数はコルーチン不要で呼び出せます。

CounterWidget.kt
 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経由の定期更新からも呼び出せます

CounterWidgetReceiver.kt
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)
    }
}
4
3
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
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?