diff --git a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/model/Poster.kt b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/model/Poster.kt index 5ec148ad..82898bd2 100644 --- a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/model/Poster.kt +++ b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/model/Poster.kt @@ -16,6 +16,7 @@ package com.github.skydoves.landscapistdemo.model import androidx.compose.runtime.Immutable +import java.util.UUID @Immutable data class Poster( @@ -24,5 +25,6 @@ data class Poster( val playtime: String, val description: String, val image: String?, - val gif: String? + val gif: String?, + val id: String = UUID.randomUUID().toString() ) diff --git a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt index e6a90084..0d1aab13 100644 --- a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt +++ b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt @@ -15,7 +15,6 @@ */ package com.github.skydoves.landscapistdemo.ui -import android.os.Build.VERSION.SDK_INT import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -48,29 +47,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.palette.graphics.Palette -import coil.ImageLoader -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder import com.github.skydoves.landscapistdemo.R import com.github.skydoves.landscapistdemo.model.MockUtil import com.github.skydoves.landscapistdemo.model.Poster import com.github.skydoves.landscapistdemo.theme.DisneyComposeTheme -import com.github.skydoves.landscapistdemo.theme.background800 -import com.github.skydoves.landscapistdemo.theme.shimmerHighLight import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.animation.circular.CircularRevealPlugin import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin -import com.skydoves.landscapist.coil.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.fresco.FrescoImage import com.skydoves.landscapist.glide.GlideImage import com.skydoves.landscapist.palette.PalettePlugin -import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin @Composable fun DisneyPosters( @@ -103,7 +94,7 @@ fun DisneyPosters( ) } } - items(items = posters) { poster -> + items(items = posters, key = { it.id }) { poster -> PosterItem(poster, vm) } } @@ -138,9 +129,8 @@ private fun SelectedPoster( var palette by remember { mutableStateOf(null) } GlideImage( - imageModel = poster.image, - modifier = Modifier - .aspectRatio(0.8f), + imageModel = { poster.image }, + modifier = Modifier.aspectRatio(0.8f), component = rememberImageComponent { +CircularRevealPlugin() +PalettePlugin { palette = it } @@ -171,26 +161,8 @@ private fun SelectedPoster( modifier = Modifier.padding(8.dp) ) - val context = LocalContext.current - val imageLoader = ImageLoader.Builder(context) - .components { - if (SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - - CoilImage( - imageModel = poster.gif, - imageLoader = { imageLoader }, - component = rememberImageComponent { - +ShimmerPlugin( - baseColor = background800, - highlightColor = shimmerHighLight - ) - }, + GlideImage( + imageModel = { poster.gif }, modifier = Modifier .height(500.dp) .padding(8.dp) diff --git a/build.gradle.kts b/build.gradle.kts index 537056d3..4e055f6e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,3 +11,19 @@ plugins { } apply(from = "${rootDir}/scripts/publish-root.gradle") + +subprojects { + tasks.withType().all { + kotlinOptions.freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + + project.buildDir.absolutePath + "/compose_metrics" + ) + kotlinOptions.freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + + project.buildDir.absolutePath + "/compose_metrics" + ) + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} diff --git a/coil/api/coil.api b/coil/api/coil.api index 3cceec0f..f7b53be2 100644 --- a/coil/api/coil.api +++ b/coil/api/coil.api @@ -1,14 +1,13 @@ public final class com/skydoves/landscapist/coil/CoilImage { - public static final fun CoilImage (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V - public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Landroid/content/Context;Landroidx/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lcoil/request/ImageRequest$Listener;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V + public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V } public abstract class com/skydoves/landscapist/coil/CoilImageState : com/skydoves/landscapist/ImageState { - public static final field $stable I } public final class com/skydoves/landscapist/coil/CoilImageState$Failure : com/skydoves/landscapist/coil/CoilImageState { - public static final field $stable I public fun (Landroid/graphics/drawable/Drawable;Ljava/lang/Throwable;)V public final fun component1 ()Landroid/graphics/drawable/Drawable; public final fun component2 ()Ljava/lang/Throwable; @@ -22,17 +21,14 @@ public final class com/skydoves/landscapist/coil/CoilImageState$Failure : com/sk } public final class com/skydoves/landscapist/coil/CoilImageState$Loading : com/skydoves/landscapist/coil/CoilImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/coil/CoilImageState$Loading; } public final class com/skydoves/landscapist/coil/CoilImageState$None : com/skydoves/landscapist/coil/CoilImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/coil/CoilImageState$None; } public final class com/skydoves/landscapist/coil/CoilImageState$Success : com/skydoves/landscapist/coil/CoilImageState { - public static final field $stable I public fun (Landroid/graphics/drawable/Drawable;Lcom/skydoves/landscapist/DataSource;)V public final fun component1 ()Landroid/graphics/drawable/Drawable; public final fun component2 ()Lcom/skydoves/landscapist/DataSource; diff --git a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt index 97c065c4..1113801f 100644 --- a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt +++ b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt @@ -19,7 +19,6 @@ package com.skydoves.landscapist.coil -import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri @@ -40,7 +39,6 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.core.graphics.drawable.toBitmap -import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.ImageRequest import coil.request.ImageResult @@ -49,6 +47,7 @@ import com.skydoves.landscapist.ImageLoad import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.LandscapistImage +import com.skydoves.landscapist.StableHolder import com.skydoves.landscapist.components.ComposeFailureStatePlugins import com.skydoves.landscapist.components.ComposeLoadingStatePlugins import com.skydoves.landscapist.components.ComposeSuccessStatePlugins @@ -63,6 +62,71 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.channelFlow import okhttp3.HttpUrl +/** + * Load and render an image with the given [imageModel] from the network or local storage. + * + * + * @param imageModel The data model to request image. See [ImageRequest.Builder.data] for types allowed. + * @param modifier [Modifier] used to adjust the layout or drawing content. + * @param imageLoader The [ImageLoader] to use when requesting the image. + * @param component An image component that conjuncts pluggable [ImagePlugin]s. + * @param requestListener A class for monitoring the status of a request while images load. + * @param imageOptions Represents parameters to load generic [Image] Composable. + * @param onImageStateChanged An image state change listener will be triggered whenever the image state is changed. + * @param previewPlaceholder Drawable resource ID which will be displayed when this function is ran in preview mode. + * @param loading Content to be displayed when the request is in progress. + * @param success Content to be displayed when the request is succeeded. + * @param failure Content to be displayed when the request is failed. + */ +@Composable +@Deprecated( + message = "Use CoilImage(imageModel = { imageModel }..) " + + "for improving recomposition performance.", + replaceWith = ReplaceWith( + "" + + "CoilImage(\n" + + " imageModel = { imageModel },\n" + + " modifier = modifier,\n" + + " imageLoader = imageLoader,\n" + + " component = component,\n" + + " requestListener = requestListener,\n" + + " imageOptions = imageOptions,\n" + + " onImageStateChanged = onImageStateChanged,\n" + + " previewPlaceholder = previewPlaceholder,\n" + + " loading = loading,\n" + + " success = success,\n" + + " failure = failure\n" + + " )" + ) +) +public fun CoilImage( + imageModel: Any?, + modifier: Modifier = Modifier, + imageLoader: @Composable () -> ImageLoader = { LocalCoilProvider.getCoilImageLoader() }, + component: ImageComponent = rememberImageComponent {}, + requestListener: (() -> ImageRequest.Listener)? = null, + imageOptions: ImageOptions = ImageOptions(), + onImageStateChanged: (CoilImageState) -> Unit = {}, + @DrawableRes previewPlaceholder: Int = 0, + loading: @Composable (BoxScope.(imageState: CoilImageState.Loading) -> Unit)? = null, + success: @Composable (BoxScope.(imageState: CoilImageState.Success) -> Unit)? = null, + failure: @Composable (BoxScope.(imageState: CoilImageState.Failure) -> Unit)? = null +) { + CoilImage( + imageModel = { imageModel }, + modifier = modifier, + imageLoader = imageLoader, + component = component, + requestListener = requestListener, + imageOptions = imageOptions, + onImageStateChanged = onImageStateChanged, + previewPlaceholder = previewPlaceholder, + loading = loading, + success = success, + failure = failure + ) +} + /** * Load and render an image with the given [imageModel] from the network or local storage. * @@ -71,7 +135,7 @@ import okhttp3.HttpUrl * * ``` * CoilImage( - * imageModel = imageModel, + * imageModel = { imageModel }, * modifier = modifier, * imageOptions = ImageOptions(contentScale = ContentScale.Crop), * loading = { @@ -88,8 +152,6 @@ import okhttp3.HttpUrl * * @param imageModel The data model to request image. See [ImageRequest.Builder.data] for types allowed. * @param modifier [Modifier] used to adjust the layout or drawing content. - * @param context The context for creating the [ImageRequest.Builder]. - * @param lifecycleOwner The [LifecycleOwner] for constructing the [ImageRequest.Builder]. * @param imageLoader The [ImageLoader] to use when requesting the image. * @param component An image component that conjuncts pluggable [ImagePlugin]s. * @param requestListener A class for monitoring the status of a request while images load. @@ -102,13 +164,11 @@ import okhttp3.HttpUrl */ @Composable public fun CoilImage( - imageModel: Any?, + imageModel: () -> Any?, modifier: Modifier = Modifier, - context: Context = LocalContext.current, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, imageLoader: @Composable () -> ImageLoader = { LocalCoilProvider.getCoilImageLoader() }, component: ImageComponent = rememberImageComponent {}, - requestListener: ImageRequest.Listener? = null, + requestListener: (() -> ImageRequest.Listener)? = null, imageOptions: ImageOptions = ImageOptions(), onImageStateChanged: (CoilImageState) -> Unit = {}, @DrawableRes previewPlaceholder: Int = 0, @@ -116,12 +176,16 @@ public fun CoilImage( success: @Composable (BoxScope.(imageState: CoilImageState.Success) -> Unit)? = null, failure: @Composable (BoxScope.(imageState: CoilImageState.Failure) -> Unit)? = null ) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current CoilImage( - imageRequest = ImageRequest.Builder(context) - .data(imageModel) - .listener(requestListener) - .lifecycle(lifecycleOwner) - .build(), + imageRequest = { + ImageRequest.Builder(context) + .data(imageModel.invoke()) + .listener(requestListener?.invoke()) + .lifecycle(lifecycleOwner) + .build() + }, imageLoader = imageLoader, component = component, modifier = modifier, @@ -169,7 +233,7 @@ public fun CoilImage( */ @Composable public fun CoilImage( - imageRequest: ImageRequest, + imageRequest: () -> ImageRequest, modifier: Modifier = Modifier, imageLoader: @Composable () -> ImageLoader = { LocalCoilProvider.getCoilImageLoader() }, component: ImageComponent = rememberImageComponent {}, @@ -200,8 +264,8 @@ public fun CoilImage( } CoilImage( - recomposeKey = imageRequest, - imageLoader = imageLoader.invoke(), + recomposeKey = StableHolder(imageRequest.invoke()), + imageLoader = StableHolder(imageLoader.invoke()), modifier = modifier ) ImageRequest@{ imageState -> when (val coilImageState = imageState.toCoilImageState().apply { internalState = this }) { @@ -224,7 +288,7 @@ public fun CoilImage( is CoilImageState.Success -> { component.ComposeSuccessStatePlugins( modifier = modifier, - imageModel = imageRequest.data, + imageModel = imageRequest.invoke().data, imageOptions = imageOptions, imageBitmap = coilImageState.drawable?.toBitmap() ?.copy(Bitmap.Config.ARGB_8888, true)?.asImageBitmap() @@ -251,7 +315,7 @@ public fun CoilImage( * * ``` * CoilImage( - * imageRequest = ImageRequest.Builder(context) + * recomposeKey = ImageRequest.Builder(context) * .data(imageModel) * .lifecycle(lifecycleOwner) * .build(), @@ -273,22 +337,22 @@ public fun CoilImage( */ @Composable private fun CoilImage( - recomposeKey: ImageRequest, + recomposeKey: StableHolder, modifier: Modifier = Modifier, - imageLoader: ImageLoader = LocalCoilProvider.getCoilImageLoader(), + imageLoader: StableHolder = StableHolder(LocalCoilProvider.getCoilImageLoader()), content: @Composable BoxScope.(imageState: ImageLoadState) -> Unit ) { val context = LocalContext.current ImageLoad( - recomposeKey = recomposeKey, + recomposeKey = recomposeKey.value, executeImageRequest = { channelFlow { - recomposeKey.newBuilder(context).target( + recomposeKey.value.newBuilder(context).target( onStart = { trySendBlocking(ImageLoadState.Loading) } ).build() - val result = imageLoader.execute(recomposeKey).toResult() + val result = imageLoader.value.execute(recomposeKey.value).toResult() send(result) } }, diff --git a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImageState.kt b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImageState.kt index d85bf5c3..a536b933 100644 --- a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImageState.kt +++ b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImageState.kt @@ -16,24 +16,30 @@ package com.skydoves.landscapist.coil import android.graphics.drawable.Drawable +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.ImageBitmap import com.skydoves.landscapist.DataSource import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageState /** GlideImageState represents the image loading states for Coil. */ +@Immutable public sealed class CoilImageState : ImageState { /** Request not started. */ + @Immutable public object None : CoilImageState() /** Request is currently in progress. */ + @Immutable public object Loading : CoilImageState() /** Request is completed successfully and ready to use an [ImageBitmap]. */ + @Immutable public data class Success(val drawable: Drawable?, val dataSource: DataSource) : CoilImageState() /** Request failed. */ + @Immutable public data class Failure(val errorDrawable: Drawable?, val reason: Throwable?) : CoilImageState() } diff --git a/fresco-websupport/api/fresco-websupport.api b/fresco-websupport/api/fresco-websupport.api index 09fec04c..435f08e3 100644 --- a/fresco-websupport/api/fresco-websupport.api +++ b/fresco-websupport/api/fresco-websupport.api @@ -1,4 +1,4 @@ public final class com/skydoves/landscapist/fresco/websupport/FrescoWebImage { - public static final fun FrescoWebImage (Lcom/facebook/drawee/backends/pipeline/PipelineDraweeControllerBuilder;Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;ILandroidx/compose/runtime/Composer;II)V + public static final fun FrescoWebImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;ILandroidx/compose/runtime/Composer;II)V } diff --git a/fresco-websupport/src/main/kotlin/com/skydoves/landscapist/fresco/websupport/FrescoWebImage.kt b/fresco-websupport/src/main/kotlin/com/skydoves/landscapist/fresco/websupport/FrescoWebImage.kt index 46b79062..e080600e 100644 --- a/fresco-websupport/src/main/kotlin/com/skydoves/landscapist/fresco/websupport/FrescoWebImage.kt +++ b/fresco-websupport/src/main/kotlin/com/skydoves/landscapist/fresco/websupport/FrescoWebImage.kt @@ -44,9 +44,11 @@ import com.skydoves.landscapist.rememberDrawablePainter * * ``` * FrescoWebImage( - * controllerBuilder = Fresco.newDraweeControllerBuilder() + * controllerBuilder = { + * Fresco.newDraweeControllerBuilder() * .setUri("asset:///animatable.webp") - * .setAutoPlayAnimations(true), + * .setAutoPlayAnimations(true) + * }, * modifier = Modifier * .width(300.dp) * .height(300.dp) @@ -77,7 +79,7 @@ import com.skydoves.landscapist.rememberDrawablePainter */ @Composable public fun FrescoWebImage( - controllerBuilder: PipelineDraweeControllerBuilder, + controllerBuilder: () -> PipelineDraweeControllerBuilder, modifier: Modifier = Modifier, contentDescription: String? = null, alignment: Alignment = Alignment.Center, @@ -102,8 +104,8 @@ public fun FrescoWebImage( val context = LocalContext.current val hierarchy = GenericDraweeHierarchyInflater.inflateBuilder(context, null).build() val holder: DraweeHolder = DraweeHolder.create(hierarchy, context) - controllerBuilder.oldController = holder.controller - holder.controller = controllerBuilder.build() + controllerBuilder().oldController = holder.controller + holder.controller = controllerBuilder().build() val topLevelDrawable = holder.topLevelDrawable Box(modifier = modifier) { diff --git a/fresco/api/fresco.api b/fresco/api/fresco.api index 922bf683..bd3be040 100644 --- a/fresco/api/fresco.api +++ b/fresco/api/fresco.api @@ -3,11 +3,9 @@ public final class com/skydoves/landscapist/fresco/FrescoImage { } public abstract class com/skydoves/landscapist/fresco/FrescoImageState : com/skydoves/landscapist/ImageState { - public static final field $stable I } public final class com/skydoves/landscapist/fresco/FrescoImageState$Failure : com/skydoves/landscapist/fresco/FrescoImageState { - public static final field $stable I public fun (Lcom/facebook/datasource/DataSource;Ljava/lang/Throwable;)V public final fun component1 ()Lcom/facebook/datasource/DataSource; public final fun component2 ()Ljava/lang/Throwable; @@ -21,17 +19,14 @@ public final class com/skydoves/landscapist/fresco/FrescoImageState$Failure : co } public final class com/skydoves/landscapist/fresco/FrescoImageState$Loading : com/skydoves/landscapist/fresco/FrescoImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/fresco/FrescoImageState$Loading; } public final class com/skydoves/landscapist/fresco/FrescoImageState$None : com/skydoves/landscapist/fresco/FrescoImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/fresco/FrescoImageState$None; } public final class com/skydoves/landscapist/fresco/FrescoImageState$Success : com/skydoves/landscapist/fresco/FrescoImageState { - public static final field $stable I public fun (Landroidx/compose/ui/graphics/ImageBitmap;Lcom/skydoves/landscapist/DataSource;)V public final fun component1 ()Landroidx/compose/ui/graphics/ImageBitmap; public final fun component2 ()Lcom/skydoves/landscapist/DataSource; diff --git a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt index 3340c56f..91ce2bc2 100644 --- a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt +++ b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt @@ -40,6 +40,7 @@ import com.skydoves.landscapist.ImageLoad import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.LandscapistImage +import com.skydoves.landscapist.StableHolder import com.skydoves.landscapist.components.ComposeFailureStatePlugins import com.skydoves.landscapist.components.ComposeLoadingStatePlugins import com.skydoves.landscapist.components.ComposeSuccessStatePlugins @@ -116,7 +117,7 @@ public fun FrescoImage( FrescoImage( recomposeKey = imageUrl, - imageRequest = imageRequest.invoke(), + imageRequest = StableHolder(imageRequest.invoke()), modifier = modifier ) ImageRequest@{ imageState -> when (val frescoImageState = imageState.toFrescoImageState().apply { internalState = this }) { @@ -185,7 +186,7 @@ public fun FrescoImage( @Composable private fun FrescoImage( recomposeKey: String?, - imageRequest: ImageRequest, + imageRequest: StableHolder, modifier: Modifier = Modifier, content: @Composable BoxScope.(imageState: ImageLoadState) -> Unit ) { @@ -200,7 +201,7 @@ private fun FrescoImage( val datasource = remember(recomposeKey) { imagePipeline.fetchDecodedImage( - imageRequest, + imageRequest.value, context, imageOriginRequestListener ) diff --git a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImageState.kt b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImageState.kt index 01935747..b99a2843 100644 --- a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImageState.kt +++ b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImageState.kt @@ -15,6 +15,7 @@ */ package com.skydoves.landscapist.fresco +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.ImageBitmap import com.facebook.common.references.CloseableReference import com.facebook.datasource.DataSource @@ -23,21 +24,26 @@ import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageState /** GlideImageState represents the image loading states for Fresco. */ +@Immutable public sealed class FrescoImageState : ImageState { /** Request not started. */ + @Immutable public object None : FrescoImageState() /** Request is currently in progress. */ + @Immutable public object Loading : FrescoImageState() /** Request is completed successfully and ready to use an [ImageBitmap]. */ + @Immutable public data class Success( val imageBitmap: ImageBitmap?, val dataSource: com.skydoves.landscapist.DataSource ) : FrescoImageState() /** Request failed. */ + @Immutable public data class Failure( val dataSource: DataSource>?, val reason: Throwable? diff --git a/glide/api/glide.api b/glide/api/glide.api index b1f5faec..b0fbe07e 100644 --- a/glide/api/glide.api +++ b/glide/api/glide.api @@ -1,13 +1,12 @@ public final class com/skydoves/landscapist/glide/GlideImage { - public static final fun GlideImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/bumptech/glide/request/RequestListener;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V + public static final fun GlideImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V + public static final fun GlideImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V } public abstract class com/skydoves/landscapist/glide/GlideImageState : com/skydoves/landscapist/ImageState { - public static final field $stable I } public final class com/skydoves/landscapist/glide/GlideImageState$Failure : com/skydoves/landscapist/glide/GlideImageState { - public static final field $stable I public fun (Landroid/graphics/drawable/Drawable;Ljava/lang/Throwable;)V public final fun component1 ()Landroid/graphics/drawable/Drawable; public final fun component2 ()Ljava/lang/Throwable; @@ -21,17 +20,14 @@ public final class com/skydoves/landscapist/glide/GlideImageState$Failure : com/ } public final class com/skydoves/landscapist/glide/GlideImageState$Loading : com/skydoves/landscapist/glide/GlideImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/glide/GlideImageState$Loading; } public final class com/skydoves/landscapist/glide/GlideImageState$None : com/skydoves/landscapist/glide/GlideImageState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/glide/GlideImageState$None; } public final class com/skydoves/landscapist/glide/GlideImageState$Success : com/skydoves/landscapist/glide/GlideImageState { - public static final field $stable I public fun (Landroid/graphics/drawable/Drawable;Lcom/skydoves/landscapist/DataSource;)V public final fun component1 ()Landroid/graphics/drawable/Drawable; public final fun component2 ()Lcom/skydoves/landscapist/DataSource; diff --git a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt index d0e28d71..c24c9a9b 100644 --- a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt +++ b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt @@ -42,6 +42,7 @@ import com.skydoves.landscapist.ImageLoad import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.LandscapistImage +import com.skydoves.landscapist.StableHolder import com.skydoves.landscapist.components.ComposeFailureStatePlugins import com.skydoves.landscapist.components.ComposeLoadingStatePlugins import com.skydoves.landscapist.components.ComposeSuccessStatePlugins @@ -53,18 +54,92 @@ import com.skydoves.landscapist.rememberDrawablePainter import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow +/** + * Load and render an image with the given [imageModel] from the network or local storage. + * + * @param imageModel The data model to request image. See [RequestBuilder.load] for types allowed. + * @param modifier [Modifier] used to adjust the layout or drawing content. + * @param requestBuilder Most options in Glide can be applied directly on the RequestBuilder object returned by Glide.with(). + * @param requestOptions Provides type independent options to customize loads with Glide. + * @param requestListener A class for monitoring the status of a request while images load. + * @param component An image component that conjuncts pluggable [ImagePlugin]s. + * @param imageOptions Represents parameters to load generic [Image] Composable. + * @param onImageStateChanged An image state change listener will be triggered whenever the image state is changed. + * @param previewPlaceholder Drawable resource ID which will be displayed when this function is ran in preview mode. + * @param loading Content to be displayed when the request is in progress. + * @param success Content to be displayed when the request is succeeded. + * @param failure Content to be displayed when the request is failed. + */ +@Deprecated( + message = "Use GlideImage(imageModel = { imageModel }..) " + + "for improving recomposition performance.", + replaceWith = ReplaceWith( + "" + + "GlideImage(\n" + + " imageModel = { imageModel },\n" + + " modifier = modifier,\n" + + " requestBuilder = requestBuilder,\n" + + " requestOptions = requestOptions,\n" + + " requestListener = requestListener,\n" + + " component = component,\n" + + " imageOptions = imageOptions,\n" + + " onImageStateChanged = onImageStateChanged,\n" + + " previewPlaceholder = previewPlaceholder,\n" + + " loading = loading,\n" + + " success = success,\n" + + " failure = failure\n" + + " )" + ) +) +@Composable +public fun GlideImage( + imageModel: Any?, + modifier: Modifier = Modifier, + requestBuilder: @Composable () -> RequestBuilder = { + LocalGlideProvider.getGlideRequestBuilder() + }, + requestOptions: @Composable () -> RequestOptions = { + LocalGlideProvider.getGlideRequestOptions() + }, + requestListener: (() -> RequestListener)? = null, + component: ImageComponent = rememberImageComponent {}, + imageOptions: ImageOptions = ImageOptions(), + onImageStateChanged: (GlideImageState) -> Unit = {}, + @DrawableRes previewPlaceholder: Int = 0, + loading: @Composable (BoxScope.(imageState: GlideImageState.Loading) -> Unit)? = null, + success: @Composable (BoxScope.(imageState: GlideImageState.Success) -> Unit)? = null, + failure: @Composable (BoxScope.(imageState: GlideImageState.Failure) -> Unit)? = null +) { + GlideImage( + imageModel = { imageModel }, + modifier = modifier, + requestBuilder = requestBuilder, + requestOptions = requestOptions, + requestListener = requestListener, + component = component, + imageOptions = imageOptions, + onImageStateChanged = onImageStateChanged, + previewPlaceholder = previewPlaceholder, + loading = loading, + success = success, + failure = failure + ) +} + /** * Load and render an image with the given [imageModel] from the network or local storage. * * ``` * GlideImage( - * imageModel = imageUrl, - * requestBuilder = Glide + * imageModel = { imageUrl }, + * requestBuilder = { + * Glide * .with(LocalContext.current) * .asBitmap() * .apply(RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL)) * .thumbnail(0.6f) - * .transition(withCrossFade()), + * .transition(withCrossFade()) + * }, * modifier = modifier, * loading = { * Box(modifier = Modifier.matchParentSize()) { @@ -93,7 +168,7 @@ import kotlinx.coroutines.flow.callbackFlow */ @Composable public fun GlideImage( - imageModel: Any?, + imageModel: () -> Any?, modifier: Modifier = Modifier, requestBuilder: @Composable () -> RequestBuilder = { LocalGlideProvider.getGlideRequestBuilder() @@ -101,7 +176,7 @@ public fun GlideImage( requestOptions: @Composable () -> RequestOptions = { LocalGlideProvider.getGlideRequestOptions() }, - requestListener: RequestListener? = null, + requestListener: (() -> RequestListener)? = null, component: ImageComponent = rememberImageComponent {}, imageOptions: ImageOptions = ImageOptions(), onImageStateChanged: (GlideImageState) -> Unit = {}, @@ -130,11 +205,13 @@ public fun GlideImage( } GlideImage( - recomposeKey = imageModel, - builder = requestBuilder.invoke() - .apply(requestOptions.invoke()) - .load(imageModel), - requestListener = requestListener, + recomposeKey = StableHolder(imageModel.invoke()), + builder = StableHolder( + requestBuilder.invoke() + .apply(requestOptions.invoke()) + .load(imageModel.invoke()) + ), + requestListener = StableHolder(requestListener?.invoke()), modifier = modifier ) ImageRequest@{ imageState -> when (val glideImageState = imageState.toGlideImageState().apply { internalState = this }) { @@ -208,16 +285,16 @@ public fun GlideImage( */ @Composable private fun GlideImage( - recomposeKey: Any?, - builder: RequestBuilder, + recomposeKey: StableHolder, modifier: Modifier = Modifier, - requestListener: RequestListener? = null, + builder: StableHolder>, + requestListener: StableHolder?> = StableHolder(null), content: @Composable BoxScope.(imageState: ImageLoadState) -> Unit ) { val requestManager = LocalGlideProvider.getGlideRequestManager() ImageLoad( - recomposeKey = recomposeKey, + recomposeKey = recomposeKey.value, executeImageRequest = { callbackFlow { val target = FlowCustomTarget(this) @@ -227,10 +304,10 @@ private fun GlideImage( // start the image request into the target. requestManager - .load(recomposeKey) - .apply(builder) + .load(recomposeKey.value) + .apply(builder.value) .addListener(flowRequestListener) - .addListener(requestListener) + .addListener(requestListener.value) .into(target) awaitClose { diff --git a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImageState.kt b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImageState.kt index 956d016e..4512d31a 100644 --- a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImageState.kt +++ b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImageState.kt @@ -16,24 +16,30 @@ package com.skydoves.landscapist.glide import android.graphics.drawable.Drawable +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.ImageBitmap import com.skydoves.landscapist.DataSource import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageState /** GlideImageState represents the image loading states for Glide. */ +@Immutable public sealed class GlideImageState : ImageState { /** Request not started. */ + @Immutable public object None : GlideImageState() /** Request is currently in progress. */ + @Immutable public object Loading : GlideImageState() /** Request is completed successfully and ready to use an [ImageBitmap]. */ + @Immutable public data class Success(val drawable: Drawable?, val dataSource: DataSource) : GlideImageState() /** Request failed. */ + @Immutable public data class Failure(val errorDrawable: Drawable?, val reason: Throwable?) : GlideImageState() } diff --git a/landscapist-placeholder/api/landscapist-placeholder.api b/landscapist-placeholder/api/landscapist-placeholder.api index 6713ecdb..fe692018 100644 --- a/landscapist-placeholder/api/landscapist-placeholder.api +++ b/landscapist-placeholder/api/landscapist-placeholder.api @@ -32,6 +32,10 @@ public final class com/skydoves/landscapist/placeholder/shimmer/LocalShimmerPara public static final fun getLocalShimmerPlugin ()Landroidx/compose/runtime/ProvidableCompositionLocal; } +public final class com/skydoves/landscapist/placeholder/shimmer/Shimmer { + public static final fun Shimmer-0Cdfkco (Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/unit/Dp;FFFILandroidx/compose/runtime/Composer;II)V +} + public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin : com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { public synthetic fun (JJLandroidx/compose/ui/unit/Dp;FFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (JJLandroidx/compose/ui/unit/Dp;FFFILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt b/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt index e9b2ab47..cc2e535a 100644 --- a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt +++ b/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt @@ -60,7 +60,7 @@ import kotlin.math.tan * @param durationMillis animation duration of the shimmering start to end. */ @Composable -internal fun Shimmer( +public fun Shimmer( modifier: Modifier = Modifier, baseColor: Color, highlightColor: Color, diff --git a/landscapist/api/landscapist.api b/landscapist/api/landscapist.api index 9dc83ffe..298afd69 100644 --- a/landscapist/api/landscapist.api +++ b/landscapist/api/landscapist.api @@ -16,11 +16,9 @@ public final class com/skydoves/landscapist/ImageLoad { } public abstract class com/skydoves/landscapist/ImageLoadState { - public static final field $stable I } public final class com/skydoves/landscapist/ImageLoadState$Failure : com/skydoves/landscapist/ImageLoadState { - public static final field $stable I public fun (Ljava/lang/Object;Ljava/lang/Throwable;)V public final fun component1 ()Ljava/lang/Object; public final fun component2 ()Ljava/lang/Throwable; @@ -34,17 +32,14 @@ public final class com/skydoves/landscapist/ImageLoadState$Failure : com/skydove } public final class com/skydoves/landscapist/ImageLoadState$Loading : com/skydoves/landscapist/ImageLoadState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/ImageLoadState$Loading; } public final class com/skydoves/landscapist/ImageLoadState$None : com/skydoves/landscapist/ImageLoadState { - public static final field $stable I public static final field INSTANCE Lcom/skydoves/landscapist/ImageLoadState$None; } public final class com/skydoves/landscapist/ImageLoadState$Success : com/skydoves/landscapist/ImageLoadState { - public static final field $stable I public fun (Ljava/lang/Object;Lcom/skydoves/landscapist/DataSource;)V public final fun component1 ()Ljava/lang/Object; public final fun component2 ()Lcom/skydoves/landscapist/DataSource; @@ -97,6 +92,12 @@ public final class com/skydoves/landscapist/RememberPainterPluginsKt { public static final fun rememberDrawablePainter (Landroid/graphics/drawable/Drawable;Ljava/util/List;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter; } +public final class com/skydoves/landscapist/StableHolder { + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public final fun getValue ()Ljava/lang/Object; +} + public abstract interface class com/skydoves/landscapist/components/ImageComponent { } diff --git a/landscapist/src/main/kotlin/com/skydoves/landscapist/ImageLoadState.kt b/landscapist/src/main/kotlin/com/skydoves/landscapist/ImageLoadState.kt index bde6c27a..66501f29 100644 --- a/landscapist/src/main/kotlin/com/skydoves/landscapist/ImageLoadState.kt +++ b/landscapist/src/main/kotlin/com/skydoves/landscapist/ImageLoadState.kt @@ -15,20 +15,26 @@ */ package com.skydoves.landscapist +import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.ImageBitmap /** ImageLoadState is a generic interface that represents image loading states. */ +@Immutable public sealed class ImageLoadState { /** Request not started. */ + @Immutable public object None : ImageLoadState() /** Request is currently in progress. */ + @Immutable public object Loading : ImageLoadState() /** Request is completed successfully and ready to use an [ImageBitmap]. */ + @Immutable public data class Success(val data: Any?, val dataSource: DataSource) : ImageLoadState() /** Request failed. */ + @Immutable public data class Failure(val data: Any?, val reason: Throwable?) : ImageLoadState() } diff --git a/landscapist/src/main/kotlin/com/skydoves/landscapist/StableHolder.kt b/landscapist/src/main/kotlin/com/skydoves/landscapist/StableHolder.kt new file mode 100644 index 00000000..3786d459 --- /dev/null +++ b/landscapist/src/main/kotlin/com/skydoves/landscapist/StableHolder.kt @@ -0,0 +1,24 @@ +/* + * Designed and developed by 2020-2022 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist + +import androidx.compose.runtime.Stable + +@Stable +@InternalLandscapistApi +public class StableHolder(public val value: T) { + public operator fun component1(): T = value +}