Jetpack Glanceを調べていたら、非常に優れたコードを発見したので紹介します。
goAsync
という拡張関数です。なぜこれがinternalなんでしょう?知見の詰まった素晴らしいコードです。
internal fun BroadcastReceiver.goAsync(
coroutineContext: CoroutineContext = Dispatchers.Default,
block: suspend CoroutineScope.() -> Unit,
) {
val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext)
val pendingResult = goAsync()
coroutineScope.launch {
try {
try {
block()
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e(GlanceAppWidgetTag, "BroadcastReceiver execution failed", t)
} finally {
// Nothing can be in the `finally` block after this, as this throws a
// `CancellationException`
coroutineScope.cancel()
}
} finally {
// This must be the last call, as the process may be killed after calling this.
try {
pendingResult.finish()
} catch (e: IllegalStateException) {
// On some OEM devices, this may throw an error about "Broadcast already finished".
// See b/257513022.
Log.e(GlanceAppWidgetTag, "Error thrown when trying to finish broadcast", e)
}
}
}
}
Jetpack Glanceでは以下のように使われています。
@CallSuper
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(
TAG,
"Using Glance in devices with API<23 is untested and might behave unexpectedly."
)
}
goAsync(coroutineContext) {
updateManager(context)
appWidgetIds.map { async { glanceAppWidget.update(context, it) } }.awaitAll()
}
}
これは何
BroadcastReceiverでIntentを受け取った後、メインスレッド以外で処理を実行したり、suspend関数を実行したい場合がよくあります。このようなシチュエーションでは、以下のようにonReceive
で非同期処理を起動し、そのままonReceive
メソッドを終了させてしまっていませんか?私は以前、普通にそうしてしまっていました。
class SomeReceiver: BroadcastReceiver() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onReceive(context: Context, intent: Intent) {
scope.launch {
suspendFoo()
}
}
}
BroadcastReceiverのライフサイクルは、原則onReceiveメソッドの実行中だけが保証されています。onRecieveで非同期処理を起動し、onReceiveが終了してしまうと、非同期処理が残っていても、OSは既に終了したものと見なしてしまう可能性があります。
ほとんどの場合、起動される非同期処理は1秒とかからず終了するでしょうから、このような実装でも問題となることはあまりないとは思いますが、良い実装とは言えません。
BroadcastReceiverで非同期処理を行う仕組みは、API 11(Android 3.0)から用意されています。
このメソッドを実行すると、PendingResultが返却され、PendingResult.finish()
が呼び出されるまでの間、BroadcastReceiverの実行を保留させることができます。
以上を踏まえて、CoroutineBroadcastReceiver.ktの実装を見てみましょう。
internal fun BroadcastReceiver.goAsync(
coroutineContext: CoroutineContext = Dispatchers.Default,
block: suspend CoroutineScope.() -> Unit,
) {
val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext)
val pendingResult = goAsync()
coroutineScope.launch {
try {
try {
block()
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e(GlanceAppWidgetTag, "BroadcastReceiver execution failed", t)
} finally {
// Nothing can be in the `finally` block after this, as this throws a
// `CancellationException`
coroutineScope.cancel()
}
} finally {
// This must be the last call, as the process may be killed after calling this.
try {
pendingResult.finish()
} catch (e: IllegalStateException) {
// On some OEM devices, this may throw an error about "Broadcast already finished".
// See b/257513022.
Log.e(GlanceAppWidgetTag, "Error thrown when trying to finish broadcast", e)
}
}
}
}
CoroutineScopeはメソッド内で作成され、実行が完了するとcancelされています。
BroadcastReceiverのライフサイクルが1メソッドの実行中に限られるので、scopeもその範囲内に閉じています。
処理の実行はtryブロックが二重になっています。
内側のブロックでは、CancellationExceptionは外部に投げ、それ以外のThrowableは握りつぶしています。(ここが汎用メソッドになっていない理由かもしれません。)そして、finallyでcoroutineScopeをcancelしています。CancellationExceptionが投げられるので外側のfinallyにまとめられないとコメントされています。
外側のfinallyでpendingResult.finish()
を実行します。このとき、一部デバイスでIllegalStateExceptionが投げられるため、それをcatchしています。これも貴重な知見ですね。
注意点
2回目のBroadcastReceiver.goAsync()
はnullが返るようになります。
BroadcastReceiver.goAsync()
を実行すると、BroadcastReceiver
自体の結果を受けとる機能をPendingResultに委譲してしまいます。2回目はすでに委譲が行われているため、新たにPendingResultを返すことはできないわけですね。
非同期実行を行う場合は、処理を一つにまとめて、すべて完了してからPendingResult.finish()
を呼び出す必要があります。複雑な処理を実行する場合は、Workerなど他の仕組みを使うべきですね。
ちょっと困ってしまうのが、Jetpack GlanceのonUpdateなどで、Glanceのsuspend関数をつかってロギングなど軽い処理を行いたい場合です。goAsync()
はGlanceAppWidgetReceiver
側で使われてしまっているので、PendingResultを受け取れません。別のBroadcastReceiverに処理を渡すか、Workerを起動するかが必要ですが、本当に小さな処理なのに、と悩ましいところです。