Introduce a MainCoroutineDispatcher (#1744)

* Introduce a `MainCoroutineDispatcher`

This is almost entirely based on
8c27d51025/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt,
which is the implementation of `kotlinx-coroutines-android`

* Lint

* Move dispatcher to AndroidCompat
This commit is contained in:
Constantin Piber
2025-10-25 20:31:59 +02:00
committed by GitHub
parent c4b2f8582e
commit 21e64eb54a

View File

@@ -0,0 +1,181 @@
@file:Suppress("UNUSED")
package kotlinx.coroutines.android
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.NonDisposableHandle
import kotlinx.coroutines.cancel
import kotlinx.coroutines.internal.MainDispatcherFactory
import kotlin.coroutines.CoroutineContext
/**
* Dispatches execution onto Android [Handler].
*
* This class provides type-safety and a point for future extensions.
*/
@OptIn(InternalCoroutinesApi::class)
public sealed class HandlerDispatcher :
MainCoroutineDispatcher(),
Delay {
/**
* Returns dispatcher that executes coroutines immediately when it is already in the right context
* (current looper is the same as this handler's looper) without an additional [re-dispatch][CoroutineDispatcher.dispatch].
* This dispatcher does not use [Handler.post] when current looper is the same as looper of the handler.
*
* Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
* The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
*
* Example of usage:
* ```
* suspend fun updateUiElement(val text: String) {
* /*
* * If it is known that updateUiElement can be invoked both from the Main thread and from other threads,
* * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch.
* *
* * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be
* * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via
* * `Handler.post` will be triggered.
* */
* withContext(Dispatchers.Main.immediate) {
* uiElement.text = text
* }
* // Do context-independent logic such as logging
* }
* ```
*/
public abstract override val immediate: HandlerDispatcher
}
@OptIn(InternalCoroutinesApi::class)
internal class AndroidDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
return HandlerContext(mainLooper.asHandler())
}
override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
override val loadPriority: Int
get() = Int.MAX_VALUE / 2
}
/**
* Represents an arbitrary [Handler] as an implementation of [CoroutineDispatcher]
* with an optional [name] for nicer debugging
*
* ## Rejected execution
*
* If the underlying handler is closed and its message-scheduling methods start to return `false` on
* an attempt to submit a continuation task to the resulting dispatcher,
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
*/
@JvmName("from") // this is for a nice Java API, see issue #255
@JvmOverloads
public fun Handler.asCoroutineDispatcher(name: String? = null): HandlerDispatcher =
HandlerContext(this, name)
private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android
internal fun Looper.asHandler(): Handler = Handler(this)
@JvmField
@Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler()) }.getOrNull()
/**
* Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
*/
@OptIn(InternalCoroutinesApi::class)
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean,
) : HandlerDispatcher(),
Delay {
/**
* Creates [CoroutineDispatcher] for the given Android [handler].
*
* @param handler a handler.
* @param name an optional name for debugging.
*/
constructor(
handler: Handler,
name: String? = null,
) : this(handler, name, false)
override val immediate: HandlerContext =
if (invokeImmediately) {
this
} else {
HandlerContext(handler, name, true)
}
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately || Looper.myLooper() != handler.looper
override fun dispatch(
context: CoroutineContext,
block: Runnable,
) {
if (!handler.post(block)) {
cancelOnRejection(context, block)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>,
) {
val block =
Runnable {
with(continuation) { resumeUndispatched(Unit) }
}
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
continuation.invokeOnCancellation { handler.removeCallbacks(block) }
} else {
cancelOnRejection(continuation.context, block)
}
}
override fun invokeOnTimeout(
timeMillis: Long,
block: Runnable,
context: CoroutineContext,
): DisposableHandle {
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
return DisposableHandle { handler.removeCallbacks(block) }
}
cancelOnRejection(context, block)
return NonDisposableHandle
}
private fun cancelOnRejection(
context: CoroutineContext,
block: Runnable,
) {
context.cancel(CancellationException("The task was rejected, the handler underlying the dispatcher '${toString()}' was closed"))
Dispatchers.IO.dispatch(context, block)
}
override fun toString(): String =
toStringInternalImpl() ?: run {
val str = name ?: handler.toString()
if (invokeImmediately) "$str.immediate" else str
}
override fun equals(other: Any?): Boolean =
other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately
// inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
}