Posted by: Jeawoong Eum
A new modern UI toolkit Jetpack Compose has been announced by Google over a year ago, and finally, it has been released 1.0 stable in July. Also many kinds of companies like Twitter, lyft, Square already have been adapted the Jetpack Compose in their production levels because it is very intuitive, powerful, and simplifies the entire UI structure if we use it well. This new paradigm of the UI structure will change a lot of things in our future, also we have to do more efforts to migrate the previous UI-related things like loading images from Url.
Loading and drawing images for Jetpack Compose
When Google announced Jetpack Compose 1.0.0-alpha01, I was wondered that how should we migrate all of the previous UI-related systems to Jetpack Compose projects? Simply fetching images from an Url (network) and drawing on the Image
composable would require a completely different process from before. At that time, accompanist by Chris Banes has been released and I was got many inspired by the library. The library supported Coil (Glide was supported later), but I wanted to use and provide as many as possible options to choose image-loading libraries. Because migrating the entire image loading systems (e.g., from Glide to Coil) could give a hundred of pain to developers. So Landscapist was created to support many options like Glide, Coil, and Fresco for Jetpack Compose.
Landscapist
Landscapist is an image loading library for Jetpack Compose. There are three options; Glide, Coil, and Fresco. So we can choose by our preference. This library also supports loading animations like shimmer effect and circular reveal. If you want to look up more usages of this library, you can reference demo projects that using Landscapist for drawing images.
GlideImage
We can load and draw images from an Url using a GlideImage
composable function like the below. That’s quite simple.
GlideImage( | |
imageModel = poster.poster | |
) |
We also can give basic attributes same as the Image
composable like contentScale
and modifier
.
GlideImage( | |
imageModel = poster.poster, | |
contentScale = ContentScale.Crop, | |
modifier = Modifier | |
) |
If we want to show a placeholder (loading) and an error image based on the fetching state, we can use the placeHolder
and error
attributes like the below.
GlideImage( | |
imageModel = imageUrl, | |
// Crop, Fit, Inside, FillHeight, FillWidth, None | |
contentScale = ContentScale.Crop, | |
// shows an image with a circular revealed animation. | |
circularRevealedEnabled = true, | |
// shows a placeholder ImageBitmap when loading. | |
placeHolder = ImageBitmap.imageResource(R.drawable.placeholder), | |
// shows an error ImageBitmap when the request failed. | |
error = ImageBitmap.imageResource(R.drawable.error) | |
) |
Different Composable based on requesting states
Sometimes we need to implement different UI based on requesting states. Basically, we can implement different composables based on the loading/success/error states. The below example shows an indicator when the requesting state is loading, and it will show a Text
composable when it goes fail. If the request succeeds, it will show a loaded image.
If we want to custom your success composable, we can also custom the success UI result like the below. The success
lambda passes a state of the requesting, and we can get an ImageBitmap
from the state instance.
LocalGlideRequestBuilder
CompositionLocal is one of the most used concepts in the Jetpack Compose, and the official Android reference describes it like the below.
Compose passes data through the composition tree explicitly through means of parameters to composable functions. This is often times the simplest and best way to have data flow through the tree. CompositionLocals can be used as an implicit way to have data flow through a composition.
Landscapist also supports the CompositionLocal
for providing like RequestBuilder
(the backbone of the request in Glide) in the composable data flow.
CoilImage
Coil is almost the same as the Glide, it uses CoilImage
instead of the GlideImage
.
Also we can give many kinds of custom attributes like Glide.
LocalCoilImageLoader
Also supports the CompositionLocal
for providing like ImageLoader
in the composable hierarchy.
FrescoImage
Fresco is a little different from the above, we should initialize an image pipeline before to use. If we need to fetch images from the network, recommend using OkHttpImagePipelineConfigFactory
.
By using an ImagePipelineConfig
, we can customize caching, networking, and thread pool strategies. Here are more references related to the pipeline config.
class App : Application() { | |
override fun onCreate() { | |
super.onCreate() | |
val pipelineConfig = | |
OkHttpImagePipelineConfigFactory | |
.newBuilder(this, OkHttpClient.Builder().build()) | |
.setDiskCacheEnabled(true) | |
.setDownsampleEnabled(true) | |
.setResizeAndRotateEnabledForNetwork(true) | |
.build() | |
Fresco.initialize(this, pipelineConfig) | |
} | |
} |
Also other things are almost the same as the above libraries.
FrescoImage( | |
imageUrl = stringImageUrl, | |
// Crop, Fit, Inside, FillHeight, FillWidth, None | |
contentScale = ContentScale.Crop, | |
// shows an image with a circular revealed animation. | |
circularRevealedEnabled = true, | |
// shows a placeholder ImageBitmap when loading. | |
placeHolder = ImageBitmap.imageResource(R.drawable.placeholder), | |
// shows an error ImageBitmap when the request failed. | |
error = ImageBitmap.imageResource(R.drawable.error) | |
) |
Job Offers
Shimmer effect
Landscapist supports shimmer effect when loading images from network, and we can implement it using a ShimmerParams
. The below example uses CoilImage
but we can also apply the ShimmerParams
for GlideImage
and FrescoImage
.
CoilImage( | |
imageModel = poster.poster, | |
modifier = modifier, | |
// shows a shimmering effect when loading an image. | |
shimmerParams = ShimmerParams( | |
baseColor = MaterialTheme.colors.background, | |
highlightColor = shimmerHighLight, | |
durationMillis = 350, | |
dropOff = 0.65f, | |
tilt = 20f | |
), | |
// shows an error text message when request failed. | |
failure = { | |
Text(text = "image request failed.") | |
}) |
We can customize the details of the ShimmerParam
using baseColor
, highlightColor
, durationMillis
, dropOff
, and tilt
.
Circular reveal animation
Landscapist supports circular reveal animation when showing images. This can be implemented very simply by giving the circularRevealedEnabled
attribute as true
.
FrescoImage( | |
imageUrl = stringImageUrl, | |
// Crop, Fit, Inside, FillHeight, FillWidth, None | |
contentScale = ContentScale.Crop, | |
// shows an image with a circular reveal animation. | |
circularRevealEnabled = true, | |
// shows a placeholder ImageBitmap when loading. | |
placeHolder = ImageBitmap.imageResource(R.drawable.placeholder), | |
// shows an error ImageBitmap when the request failed. | |
error = ImageBitmap.imageResource(R.drawable.error) | |
) |
CircularRevealedImage
We can use CircularRevealedImage
composable regardless of image loading libraries (Glide, Coil, Fresco) and we can implement circular reveal animation using our drawable resource image.
CircularRevealedImage( | |
bitmap = ImageBitmap.imageResource(R.drawable.flower), | |
contentDescription = null, | |
circularRevealEnabled = true, | |
circularRevealDuration = 350 | |
) |
We should set the circularRevealedEnabled
as true
if we want to apply circular reveal animation, also we can change the duration of the animation using circularRevealedDuration
attribute.
Palette
Landscapist supports palette APIs for extracting color profiles from images. Basically, we should use BitmapPalette
to extract major colors from images like the below. And we can reference color types here.
var palette by remember { mutableStateOf<Palette?>(null) } | |
GlideImage( // CoilImage, FrescoImage also can be used. | |
imageModel = poster?.poster!!, | |
bitmapPalette = BitmapPalette { | |
palette = it | |
} | |
) | |
Crossfade( | |
targetState = palette, | |
modifier = Modifier | |
.padding(horizontal = 8.dp) | |
.size(45.dp) | |
) { | |
Box( | |
modifier = Modifier | |
.background(color = Color(it?.lightVibrantSwatch?.rgb ?: 0)) | |
.fillMaxSize() | |
) | |
} |
We can customize more options using like interceptor
and paletteLoadListener
.
var palette by remember { mutableStateOf<Palette?>(null) } | |
GlideImage( | |
imageModel = poster?.poster!!, | |
modifier = Modifier | |
.aspectRatio(0.8f), | |
bitmapPalette = BitmapPalette( | |
imageModel = poster.poster, | |
useCache = true, | |
interceptor = { | |
it.addFilter { rgb, hsl -> | |
// here edit to add the filter colors. | |
false | |
} | |
}, | |
paletteLoadedListener = { | |
palette = it | |
} | |
) | |
) |
Conclusion
In this post, we looked around how to load and draw images for Jetpack Compose using Landscapist. Very thankfully, this library has been used by many global companies including Twitter. This journey started from the Jetpack Compose 1.0.0-alpha
and now it has been released more than 30 times until reaches the Jetpack Compose 1.0 stable. Luckily, I got a lot of inspired and helped from accompanist (Thank you, Chris Banes! and thanks for all the Google Compose, Android Team!), and this long journey seems to just get started. ?