Jetpack Compose was announced at Google IO 2019 and is going to change how we do UI development on Android. I’ve been working a lot with the Android image APIs as I’ve been developing Coil and was curious how it treats images and how the concept of image loaders would fit in.
In the current Android UI framework
ImageView is the main way to show an image on screen. Images are represented as
Drawables, which often (though not always) wrap
Bitmaps are raw, uncompressed pixel data stored in memory.
In Jetpack Compose there is no
ImageView because there is no
View. Instead, views are replaced by
Composables which define a composable piece of UI to add to the view hierarchy. Likewise, there are no
Drawables. Instead, it’s replaced by
Image which is a minimal interface that wraps a
NativeImage. At the moment
NativeImage is defined as a
NativeImage is prefixed with a commented out
expect declaration, which is a Kotlin Multiplatform keyword 🤔. Currently there isn’t any support for animated images, though I’d expect
AnimatedImage to be added later. Here are the rough API analogs:
At the moment, there is only one
Image creation function,
imageResource, which synchronously loads an image from resources. The API is noted as transient and will be replaced with an asynchronous API (likely using Coroutines) in the future. However, if we want to create an
Image from a URL, a file, a URI, or another data source we’ll have to write it ourselves for now. Fortunately, we can offload the heavy lifting to an image loading library.
The easiest way to accomplish this is to write an
effect. Effects are positionally memoized blocks of code that have a return value. They can be called from a
Composable and will cause the composition (basically the view hierarchy) to rebuild if its output value is updated. Here’s an
image effect implementation backed by Coil:
What do these functions do?
image(data: Any)is a simple version of
image(request: GetRequest)that uses the default options to launch an image request.
imageis called as part of a
Composableit will emit a null
Imageand begin asynchronously loading the given
- It will update the
imagestate when it’s successful and Jetpack Compose will re-render the
Composablewith the updated
- If the request is in-flight and the
Composableis removed from the composition, the request will be automatically cancelled.
Cool, now let’s take a look at the JetNews sample app. At the moment it eagerly loads all its resources in
image, we can replace all the eager loading with lazy, non-blocking, asynchronous calls. Additionally, we can replace all the hardcoded resources with URLs! Here’s what
PostImage looks like after being converted:
Great! We’re done, right? While this will theoretically work (currently Coroutines doesn’t work with the Jetpack Compose compiler), the
image function is missing a number of features and can be optimized:
- Automatic sizing: At the moment, Coil will load the image at its original size (bounded by the size of the display) since it has no way to resolve the size of the parent container. One way to solve this would be to write our own
Composableto render the images. However, that’s analogous to writing a custom
ImageViewwhich is more restrictive for API consumers.
- Bitmap pooling:
Coil.getprevents recycling the returned drawable’s
Bitmapsince Coil doesn’t know when it’s safe to return it to the pool. When you load an image into an
ImageViewCoil knows it’s safe to recycle the Bitmap when either
Lifecycle.onDestroyoccurs, or another image load request is started on that
ImageView. Jetpack Compose provides
CommitScope.onDisposeas a lifecycle callback to clean up your components and Coil (and other image loaders) will need to treat that as a valid request disposal callback.
Most of these issues stem from the clean separation between Compose and the traditional UI framework classes like
Drawable. That said, separating from those classes is absolutely the right idea since they are tied to the platform, rely on inheritance, and hold a lot of internal state (
View is almost 30k lines long!).
Image aren’t tied to the platform, favour composition over inheritance, and hold minimal to no internal state.
Overall I’m extremely excited by the progress on Jetpack Compose and look forward to ensuring Coil works effortlessly with Compose (when it’s ready). Also, if you want to see my fork of the JetNews app with the lazy loading changes, you can find it here.