Skip to content

Commit

Permalink
Guard against Dispatchers.Main.immediate being unavailable. (#2699)
Browse files Browse the repository at this point in the history
* Guard against Dispatchers.Main.immediate being unavailable.

* Avoid changing context.

* Update API.

* Fix tests.

* Clean up.

* Fixes.

* Keep fix targeted for now.

* Clean up.

* Clean up.

* Fix.
  • Loading branch information
colinrtwhite authored Nov 25, 2024
1 parent ea2d442 commit 70a23cb
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
Expand All @@ -33,8 +32,8 @@ import coil3.compose.AsyncImagePainter.State
import coil3.compose.internal.AsyncImageState
import coil3.compose.internal.CrossfadePainter
import coil3.compose.internal.onStateOf
import coil3.compose.internal.rememberImmediateCoroutineScope
import coil3.compose.internal.requestOf
import coil3.compose.internal.safeImmediateMainDispatcher
import coil3.compose.internal.toScale
import coil3.compose.internal.transformOf
import coil3.request.ErrorResult
Expand All @@ -45,7 +44,6 @@ import coil3.size.Precision
import coil3.size.SizeResolver
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
Expand Down Expand Up @@ -139,7 +137,7 @@ private fun rememberAsyncImagePainter(

val input = Input(state.imageLoader, request, state.modelEqualityDelegate)
val painter = remember { AsyncImagePainter(input) }
painter.scope = rememberCoroutineScope()
painter.scope = rememberImmediateCoroutineScope()
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
Expand Down Expand Up @@ -216,23 +214,22 @@ class AsyncImagePainter internal constructor(
(painter as? RememberObserver)?.onRemembered()

// Observe the latest request and execute any emissions.
val previewHandler = previewHandler
if (previewHandler != null) {
// If we're in inspection mode use the preview renderer.
rememberJob = scope.launch(Dispatchers.Unconfined) {
restartSignal.flatMapLatest { _input }.mapLatest {
val request = updateRequest(it.request, isPreview = true)
previewHandler.handle(it.imageLoader, request)
}.collect(::updateState)
}
} else {
// Else, execute the request as normal.
rememberJob = scope.launch(safeImmediateMainDispatcher) {
restartSignal.flatMapLatest { _input }.mapLatest {
val request = updateRequest(it.request, isPreview = false)
it.imageLoader.execute(request).toState()
}.collect(::updateState)
}
rememberJob = scope.launch {
restartSignal
.flatMapLatest { _input }
.mapLatest { input ->
val previewHandler = previewHandler
if (previewHandler != null) {
// If we're in inspection mode use the preview renderer.
val request = updateRequest(input.request, isPreview = true)
previewHandler.handle(input.imageLoader, request)
} else {
// Else, execute the request as normal.
val request = updateRequest(input.request, isPreview = false)
input.imageLoader.execute(request).toState()
}
}
.collect(::updateState)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
Expand Down Expand Up @@ -33,7 +34,10 @@ import coil3.size.SizeResolver
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainCoroutineDispatcher

/** Create an [ImageRequest] from the [model]. */
@Composable
Expand Down Expand Up @@ -212,16 +216,37 @@ internal fun Size.toIntSize() = IntSize(width.roundToInt(), height.roundToInt())

internal val Size.isPositive get() = width >= 0.5 && height >= 0.5

// We need `Dispatchers.Main.immediate` to be able to execute immediately on the main thread so we
// can reach the loading state, set the placeholder, and maybe resolve from the memory cache.
// The default main dispatcher provided with Compose always dispatches, which will often cause one
// frame of delay. In the cases where we don't have the main dispatcher implicitly fall back to
// Compose's built in main dispatcher.
internal val safeImmediateMainDispatcher: CoroutineContext = try {
// We need `Dispatchers.Main.immediate` to be able to execute immediately on the main thread so
// we can reach the loading state, set the placeholder, and maybe resolve from the memory cache.
// The default main dispatcher provided with Compose always dispatches, which will often cause
// one frame of delay. If `Dispatchers.Main.immediate` isn't available, fall back to
// `Dispatchers.Unconfined`, which will execute immediately even if we're not on the main
// thread. This will typically only occur in preview/test environments where image loading
// should execute synchronously.
private val immediateDispatcher: CoroutineDispatcher = try {
Dispatchers.Main.immediate.also {
// This will throw if the implementation is missing.
it.isDispatchNeeded(EmptyCoroutineContext)
}
} catch (_: Throwable) {
EmptyCoroutineContext
Dispatchers.Unconfined
}

@OptIn(ExperimentalStdlibApi::class)
private fun CoroutineContext.resolveImmediateDispatcher(): CoroutineDispatcher {
val dispatcher = get(CoroutineDispatcher)
if (dispatcher is MainCoroutineDispatcher) {
try {
return dispatcher.immediate
} catch (_: UnsupportedOperationException) {}
}
return immediateDispatcher
}

@Composable
internal fun rememberImmediateCoroutineScope(): CoroutineScope {
val scope = rememberCoroutineScope()
return remember(scope) {
CoroutineScope(scope.coroutineContext.run { this + resolveImmediateDispatcher() })
}
}

0 comments on commit 70a23cb

Please sign in to comment.