diff --git a/coil-compose-core/api/coil-compose-core.klib.api b/coil-compose-core/api/coil-compose-core.klib.api index fd5127f8b7..98b11da685 100644 --- a/coil-compose-core/api/coil-compose-core.klib.api +++ b/coil-compose-core/api/coil-compose-core.klib.api @@ -183,6 +183,9 @@ final class coil3.compose/ImagePainter : androidx.compose.ui.graphics.painter/Pa final val coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop // coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop|#static{}coil3_compose_internal_AsyncImageState$stableprop[0] final val coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop // coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop|#static{}coil3_compose_internal_ContentPainterElement$stableprop[0] final val coil3.compose.internal/coil3_compose_internal_ContentPainterNode$stableprop // coil3.compose.internal/coil3_compose_internal_ContentPainterNode$stableprop|#static{}coil3_compose_internal_ContentPainterNode$stableprop[0] +final val coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop // coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop|#static{}coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop[0] +final val coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop // coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop|#static{}coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop[0] +final val coil3.compose.internal/coil3_compose_internal_ForwardingCoroutineContext$stableprop // coil3.compose.internal/coil3_compose_internal_ForwardingCoroutineContext$stableprop|#static{}coil3_compose_internal_ForwardingCoroutineContext$stableprop[0] final val coil3.compose/LocalAsyncImageModelEqualityDelegate // coil3.compose/LocalAsyncImageModelEqualityDelegate|{}LocalAsyncImageModelEqualityDelegate[0] final fun (): androidx.compose.runtime/ProvidableCompositionLocal // coil3.compose/LocalAsyncImageModelEqualityDelegate.|(){}[0] final val coil3.compose/LocalAsyncImagePreviewHandler // coil3.compose/LocalAsyncImagePreviewHandler|{}LocalAsyncImagePreviewHandler[0] @@ -204,6 +207,9 @@ final fun (coil3/Image).coil3.compose/asPainter(coil3/PlatformContext, androidx. final fun coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop_getter|coil3_compose_internal_AsyncImageState$stableprop_getter(){}[0] final fun coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop_getter|coil3_compose_internal_ContentPainterElement$stableprop_getter(){}[0] final fun coil3.compose.internal/coil3_compose_internal_ContentPainterNode$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_ContentPainterNode$stableprop_getter|coil3_compose_internal_ContentPainterNode$stableprop_getter(){}[0] +final fun coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop_getter|coil3_compose_internal_DeferredDispatchCoroutineContext$stableprop_getter(){}[0] +final fun coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop_getter|coil3_compose_internal_DeferredDispatchCoroutineDispatcher$stableprop_getter(){}[0] +final fun coil3.compose.internal/coil3_compose_internal_ForwardingCoroutineContext$stableprop_getter(): kotlin/Int // coil3.compose.internal/coil3_compose_internal_ForwardingCoroutineContext$stableprop_getter|coil3_compose_internal_ForwardingCoroutineContext$stableprop_getter(){}[0] final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/DrawScopeSizeResolver(): coil3.compose/DrawScopeSizeResolver // coil3.compose/DrawScopeSizeResolver|DrawScopeSizeResolver(){}[0] diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt index 5dde717cf4..823c680f04 100644 --- a/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt @@ -10,6 +10,7 @@ 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 @@ -30,11 +31,13 @@ import coil3.compose.AsyncImagePainter.Companion.DefaultTransform import coil3.compose.AsyncImagePainter.Input import coil3.compose.AsyncImagePainter.State import coil3.compose.internal.AsyncImageState +import coil3.compose.internal.DeferredDispatchCoroutineScope +import coil3.compose.internal.launchUndispatched import coil3.compose.internal.onStateOf -import coil3.compose.internal.rememberImmediateCoroutineScope import coil3.compose.internal.requestOf import coil3.compose.internal.toScale import coil3.compose.internal.transformOf +import coil3.compose.internal.withDeferredDispatch import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.ImageResult @@ -50,9 +53,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.transformLatest /** * Return an [AsyncImagePainter] that executes an [ImageRequest] asynchronously and renders the result. @@ -136,7 +138,7 @@ private fun rememberAsyncImagePainter( val input = Input(state.imageLoader, request, state.modelEqualityDelegate) val painter = remember { AsyncImagePainter(input) } - painter.scope = rememberImmediateCoroutineScope() + painter.scope = rememberCoroutineScope() painter.transform = transform painter.onState = onState painter.contentScale = contentScale @@ -213,22 +215,24 @@ class AsyncImagePainter internal constructor( (painter as? RememberObserver)?.onRemembered() // Observe the latest request and execute any emissions. - 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() + rememberJob = DeferredDispatchCoroutineScope(scope.coroutineContext).launchUndispatched { + restartSignal.transformLatest { + _input.collect { input -> + withDeferredDispatch { + val previewHandler = previewHandler + val state = 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() + } + updateState(state) } } - .collect(::updateState) + }.collect() } } diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/DeferredDispatch.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/DeferredDispatch.kt new file mode 100644 index 0000000000..1ec7f1f2ed --- /dev/null +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/DeferredDispatch.kt @@ -0,0 +1,101 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package coil3.compose.internal + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * A [CoroutineScope] that does not dispatch until the [CoroutineDispatcher] in its + * [CoroutineContext] changes. + */ +internal fun DeferredDispatchCoroutineScope( + context: CoroutineContext, +): CoroutineScope { + val originalDispatcher = context.dispatcher ?: Dispatchers.Unconfined + return CoroutineScope(DeferredDispatchCoroutineContext(context, originalDispatcher)) +} + +/** + * A special [CoroutineContext] implementation that automatically enables + * [DeferredDispatchCoroutineDispatcher] dispatching if the context's [CoroutineDispatcher] changes. + */ +internal class DeferredDispatchCoroutineContext( + context: CoroutineContext, + val originalDispatcher: CoroutineDispatcher, +) : ForwardingCoroutineContext(context) { + + override fun newContext( + old: CoroutineContext, + new: CoroutineContext, + ): ForwardingCoroutineContext { + val oldDispatcher = old.dispatcher + val newDispatcher = new.dispatcher + if (oldDispatcher is DeferredDispatchCoroutineDispatcher && oldDispatcher != newDispatcher) { + oldDispatcher.unconfined = oldDispatcher.unconfined && + (newDispatcher == null || newDispatcher == Dispatchers.Unconfined) + } + + return DeferredDispatchCoroutineContext(new, originalDispatcher) + } +} + +/** Launch [block] without dispatching. */ +internal inline fun CoroutineScope.launchUndispatched( + noinline block: suspend CoroutineScope.() -> Unit, +) = launch(Dispatchers.Unconfined, CoroutineStart.UNDISPATCHED, block) + +/** + * Execute [block] without dispatching until the scope's [CoroutineDispatcher] changes. + * Must be called from inside a [DeferredDispatchCoroutineScope]. + */ +internal suspend inline fun withDeferredDispatch( + noinline block: suspend CoroutineScope.() -> T, +): T { + val originalDispatcher = (coroutineContext as DeferredDispatchCoroutineContext).originalDispatcher + return withContext(DeferredDispatchCoroutineDispatcher(originalDispatcher), block) +} + +/** + * A [CoroutineDispatcher] that delegates to [Dispatchers.Unconfined] while [unconfined] is true + * and [delegate] when [unconfined] is false. + */ +internal class DeferredDispatchCoroutineDispatcher( + private val delegate: CoroutineDispatcher, +) : CoroutineDispatcher() { + private val _unconfined = atomic(true) + var unconfined by _unconfined + + private val currentDispatcher: CoroutineDispatcher + get() = if (_unconfined.value) Dispatchers.Unconfined else delegate + + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + return currentDispatcher.isDispatchNeeded(context) + } + + override fun limitedParallelism(parallelism: Int, name: String?): CoroutineDispatcher { + return currentDispatcher.limitedParallelism(parallelism, name) + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + currentDispatcher.dispatch(context, block) + } + + @InternalCoroutinesApi + override fun dispatchYield(context: CoroutineContext, block: Runnable) { + currentDispatcher.dispatchYield(context, block) + } + + override fun toString(): String { + return "DeferredDispatchCoroutineDispatcher(delegate=$delegate)" + } +} diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt new file mode 100644 index 0000000000..1e8fc6417a --- /dev/null +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt @@ -0,0 +1,38 @@ +package coil3.compose.internal + +import kotlin.coroutines.CoroutineContext + +/** + * A special [CoroutineContext] implementation that observes changes to its elements. + */ +internal abstract class ForwardingCoroutineContext( + private val delegate: CoroutineContext, +) : CoroutineContext by delegate { + + abstract fun newContext( + old: CoroutineContext, + new: CoroutineContext, + ): ForwardingCoroutineContext + + override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext { + val new = delegate.minusKey(key) + return newContext(this, new) + } + + override operator fun plus(context: CoroutineContext): CoroutineContext { + val new = delegate + context + return newContext(this, new) + } + + override fun equals(other: Any?): Boolean { + return delegate == other + } + + override fun hashCode(): Int { + return delegate.hashCode() + } + + override fun toString(): String { + return "ForwardingCoroutineContext(delegate=$delegate)" + } +} diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/utils.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/utils.kt index a841557f71..1c03aa7b10 100644 --- a/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/utils.kt +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/internal/utils.kt @@ -7,13 +7,11 @@ 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 import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -35,12 +33,8 @@ import coil3.size.Scale import coil3.size.Size as CoilSize 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 @@ -219,43 +213,6 @@ 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. 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) { - 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() - val isPreview = LocalInspectionMode.current - return remember(scope, isPreview) { - val context = if (isPreview) { - scope.coroutineContext + Dispatchers.Unconfined - } else { - scope.coroutineContext.run { this + resolveImmediateDispatcher() } - } - CoroutineScope(context) - } -} +internal val CoroutineContext.dispatcher: CoroutineDispatcher? + get() = get(CoroutineDispatcher) diff --git a/coil-compose-core/src/commonTest/kotlin/coil3/compose/internal/DeferredDispatchTest.kt b/coil-compose-core/src/commonTest/kotlin/coil3/compose/internal/DeferredDispatchTest.kt new file mode 100644 index 0000000000..9560075b93 --- /dev/null +++ b/coil-compose-core/src/commonTest/kotlin/coil3/compose/internal/DeferredDispatchTest.kt @@ -0,0 +1,148 @@ +package coil3.compose.internal + +import coil3.ImageLoader +import coil3.compose.AsyncImagePainter +import coil3.decode.DataSource +import coil3.decode.DecodeResult +import coil3.decode.Decoder +import coil3.decode.ImageSource +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.fetch.SourceFetchResult +import coil3.request.ImageRequest +import coil3.request.Options +import coil3.request.SuccessResult +import coil3.test.utils.FakeImage +import coil3.test.utils.RobolectricTest +import coil3.test.utils.context +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import okio.Buffer + +class DeferredDispatchTest : RobolectricTest() { + private val testDispatcher = TestCoroutineDispatcher() + private val deferredDispatcher = DeferredDispatchCoroutineDispatcher(testDispatcher) + + @Test + fun `does not dispatch when unconfined=true`() = runTest { + deferredDispatcher.unconfined = true + + withContext(deferredDispatcher) { + delay(100.milliseconds) + assertEquals(0, testDispatcher.dispatchCount) + } + } + + @Test + fun `does dispatch when unconfined=false`() = runTest { + deferredDispatcher.unconfined = false + + withContext(deferredDispatcher) { + delay(100.milliseconds) + assertEquals(2, testDispatcher.dispatchCount) + } + } + + /** This test emulates the context that [AsyncImagePainter] launches its request into. */ + @Test + fun `imageLoader does not dispatch if context does not change`() = runTest { + deferredDispatcher.unconfined = true + + DeferredDispatchCoroutineScope(coroutineContext + deferredDispatcher).launch { + val imageLoader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(Unit) + .fetcherFactory(TestFetcher.Factory()) + .decoderFactory(TestDecoder.Factory()) + .coroutineContext(EmptyCoroutineContext) + .build() + val result = imageLoader.execute(request) + + assertIs(result) + assertEquals(0, testDispatcher.dispatchCount) + }.join() + } + + /** This test emulates the context that [AsyncImagePainter] launches its request into. */ + @Test + fun `imageLoader does dispatch if context changes`() = runTest { + deferredDispatcher.unconfined = true + + DeferredDispatchCoroutineScope(coroutineContext + deferredDispatcher).launch { + val imageLoader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(Unit) + .fetcherFactory(TestFetcher.Factory()) + .decoderFactory(TestDecoder.Factory()) + .decoderCoroutineContext(Dispatchers.Default) + .build() + val result = imageLoader.execute(request) + + assertIs(result) + assertEquals(1, testDispatcher.dispatchCount) + }.join() + } + + private class TestCoroutineDispatcher : CoroutineDispatcher() { + private var _dispatchCount by atomic(0) + val dispatchCount get() = _dispatchCount + + override fun dispatch(context: CoroutineContext, block: Runnable) { + _dispatchCount++ + block.run() + } + } + + private class TestFetcher( + private val options: Options, + ) : Fetcher { + + override suspend fun fetch(): FetchResult { + return SourceFetchResult( + source = ImageSource(Buffer(), options.fileSystem), + mimeType = null, + dataSource = DataSource.MEMORY, + ) + } + + class Factory : Fetcher.Factory { + override fun create( + data: Unit, + options: Options, + imageLoader: ImageLoader, + ): Fetcher = TestFetcher(options) + } + } + + private class TestDecoder( + private val options: Options, + ) : Decoder { + + override suspend fun decode(): DecodeResult { + return DecodeResult( + image = FakeImage(), + isSampled = false, + ) + } + + class Factory : Decoder.Factory { + override fun create( + result: SourceFetchResult, + options: Options, + imageLoader: ImageLoader, + ): Decoder = TestDecoder(options) + } + } +} diff --git a/coil-compose/README.md b/coil-compose/README.md index 2ac6864127..40308a71aa 100644 --- a/coil-compose/README.md +++ b/coil-compose/README.md @@ -17,9 +17,6 @@ AsyncImage( `model` can either be the `ImageRequest.data` value - or the `ImageRequest` itself. `contentDescription` sets the text used by accessibility services to describe what this image represents. -!!! Note - If you use Compose on JVM/desktop you should import `org.jetbrains.kotlinx:kotlinx-coroutines-swing:`. Coil relies on `Dispatchers.Main.immediate` to resolve images from the memory cache synchronously and `kotlinx-coroutines-swing` provides support for that on JVM (non-Android) platforms. - ## AsyncImage `AsyncImage` is a composable that executes an image request asynchronously and renders the result. It supports the same arguments as the standard `Image` composable and additionally, it supports setting `placeholder`/`error`/`fallback` painters and `onLoading`/`onSuccess`/`onError` callbacks. Here's an example that loads an image with a circle crop, crossfade, and sets a placeholder: diff --git a/coil-core/src/androidUnitTest/kotlin/coil3/request/ImageRequestTest.kt b/coil-core/src/androidUnitTest/kotlin/coil3/request/ImageRequestTest.kt index e7f9795b06..d2abff529c 100644 --- a/coil-core/src/androidUnitTest/kotlin/coil3/request/ImageRequestTest.kt +++ b/coil-core/src/androidUnitTest/kotlin/coil3/request/ImageRequestTest.kt @@ -15,6 +15,7 @@ import coil3.test.utils.RobolectricTest import coil3.test.utils.context import coil3.transition.CrossfadeTransition import coil3.transition.Transition +import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext import kotlin.test.assertEquals import kotlin.test.assertIs @@ -189,8 +190,7 @@ class ImageRequestTest : RobolectricTest() { assertNotEquals(request1.extras, request2.extras) } - private class TestCoroutineContextMarker : CoroutineContext.Element { - override val key get() = Key + private class TestCoroutineContextMarker : AbstractCoroutineContextElement(Key) { object Key : CoroutineContext.Key } } diff --git a/docs/upgrading_to_coil3.md b/docs/upgrading_to_coil3.md index 85f50b8394..31709c58f5 100644 --- a/docs/upgrading_to_coil3.md +++ b/docs/upgrading_to_coil3.md @@ -47,9 +47,6 @@ The `coil-compose` artifact's APIs are mostly unchanged. You can continue using - `AsyncImagePainter`'s default `SizeResolver` no longer waits for the first `onDraw` call to get the size of the canvas. Instead, `AsyncImagePainter` defaults to `Size.ORIGINAL`. - The Compose `modelEqualityDelegate` delegate is now set via a composition local, `LocalAsyncImageModelEqualityDelegate`, instead of as a parameter to `AsyncImage`/`SubcomposeAsyncImage`/`rememberAsyncImagePainter`. -!!! Note - If you use Coil on a JVM (non-Android) platform, you should add a dependency on a [coroutines main dispatcher](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html). On desktop you likely want to import `org.jetbrains.kotlinx:kotlinx-coroutines-swing`. If it's not imported then `ImageRequest`s won't be dispatched immediately and will have one frame of delay before setting the `ImageRequest.placeholder` or resolving from the memory cache. - ## General Other important behavior changes include: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47635f5901..415e7473f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,6 @@ androidx-vectordrawable-animated = "androidx.vectordrawable:vectordrawable-anima coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } google-material = "com.google.android.material:material:1.12.0" diff --git a/internal/test-roborazzi/build.gradle.kts b/internal/test-roborazzi/build.gradle.kts index 9f8ff39207..b8ca5a5834 100644 --- a/internal/test-roborazzi/build.gradle.kts +++ b/internal/test-roborazzi/build.gradle.kts @@ -36,7 +36,6 @@ kotlin { named("jvmCommonTest").dependencies { implementation(compose.desktop.uiTestJUnit4) implementation(libs.bundles.test.jvm) - implementation(libs.coroutines.swing) } jvmTest.dependencies { implementation(compose.desktop.currentOs) diff --git a/samples/shared/build.gradle.kts b/samples/shared/build.gradle.kts index 998f856fd1..1a658db8c4 100644 --- a/samples/shared/build.gradle.kts +++ b/samples/shared/build.gradle.kts @@ -42,7 +42,6 @@ kotlin { jvmMain { dependencies { api(projects.coilNetworkOkhttp) - api(libs.coroutines.swing) } } named("wasmJsMain") {