Exploring Images in Jetpack Compose

Colin White
tech-at-instacart
Published in
4 min readDec 3, 2019

--

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. 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 typealias to Bitmap. Interestingly, 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:

  • View -> Composable
  • ImageView -> DrawImage
  • Drawable -> Image
  • Bitmap -> NativeImage

Creating Images

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.
  • When image is called as part of a Composable it will emit a null Image and begin asynchronously loading the given data .
  • It will update the image state when it’s successful and Jetpack Compose will re-render the Composable with the updated Image.
  • If the request is in-flight and the Composable is 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 MainActivity.onCreate. Using 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 Composable to render the images. However, that’s analogous to writing a custom ImageView which is more restrictive for API consumers.
  • Bitmap pooling: Coil.get prevents recycling the returned drawable’s Bitmap since Coil doesn’t know when it’s safe to return it to the pool. When you load an image into an ImageView Coil knows it’s safe to recycle the Bitmap when either View.onViewDetachedFromWindow occurs, Lifecycle.onDestroy occurs, or another image load request is started on that ImageView. Jetpack Compose provides CommitScope.onDispose as 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 View and 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!). Composable and 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.

--

--