MapView for Jetpack Compose
By: Pierre Laurence
Tags: Jetpack Compose, Kotlin, Android, Mapview, Maps

Jetpack compose 1.0 is finally released, and it’s now the perfect time to use it in production. This new way of building UIs comes with a comprehensive set of widgets, like e.g LazyColumns (the equivalent of RecyclerView). As more advanced libraries start to emerge, I recently faced the challenge to re-implement my MapView library to Compose. After weeks of effort, please welcome MapCompose!

W hat is MapCompose for?

If you ever had to work with maps, or with huge images, you might have heard of tiled-based images. In a nutshell, the idea is to assemble small images, called tiles, in such a way that it feels seamless — like scrolling one big image. One key optimization is to render only visible tiles, which results in less memory being used.

That works for a single level, and that doesn’t sound too complicated. In reality, however, maps (or at least, those made of tiles) have several levels, like this:

Each level is twice bigger than the former (at lower scale), and provides more details. Overall, this looks like a pyramid. If you wonder how to build such a pyramid, you’ll be happy to hear that tools exist to built them out of a single image file. Another common usage is to display maps from well-known map providers such as OpenStreetMap. They provide APIs for getting tiles given the levels, rows, and columns (these are also called WMTS — Web Map Tile Service).

Whichever is your data source (tiles on the file system, a database, or a remote WMTS provider), MapCompose is there to do the hard work of displaying the map, while supporting commun gestures like scrolling, zooming, and (optionally) rotating — as shown in the demo below.

For the user, navigating though the pyramid of tiles is seamless. When transitioning from a level to another, alpha animation progressively fades-in the new level while the former fades-out.

Usage

With Jetpack Compose, we have to change the way we think about views. In the previous View system, we had references on views and mutated their state directly. While that could be done right, the state often ended-up scattered between views own state and application state. Sometimes, it was difficult to predict how views were rendered because there were so many things to take into account.

Now, in Compose, the rendering is function of a state. If that state changes, the “view” updates accordingly. The library exposes its API though MapState , which is the only public handle to mutate the state of the "view" (or in Compose terms, "composables"). As its name suggests, MapState also owns the state. Therefore, the composables will always render consistently, even after a device rotation.

In a typical application, you create a MapState instance inside a ViewModel (or whatever component which survives device rotation). Your MapState should then be passed to the MapUI composable, as shown in the code sample below.

The first step is to create an instance of TileStreamProvider , which is an interface with a single abstract method:

In our example, the TileStreamProvider implementation looks for tiles in the assets. However, it could also have performed HTTP requests using Glide — it’s up to you. You don’t have to worry about threading ; MapCompose has a dedicated thread pool for fetching tiles in the background.

Whenever you need to update the map (add a marker, a path, change the scale, etc.), you invoke APIs on your MapState instance. In the above example, we invoked enableRotation() and scrollTo() APIs. All public APIs are located under the api package.

Markers support

A marker can be any composable you like. In the example below, we add a blue map pin, using the addMarker API.

Callout support

Callouts are typically message popups which are, like markers, attached to a specific position. However, they automatically dismiss on touch down (this is the default behavior, which can be changed). To add a callout, use addCallout.

Paths support

To add a path, follow these three steps:

It’s important to note that the only way to get a PathDataBuilder is by using the makePathDataBuilder function. Once you've built your PathData instance, you can use the use the addPath API.

Performance analysis

A good performance indicator is GPU rendering speed. In the video below, I enabled HWUI rendering on screen as bars.

Under the hood, MapCompose makes use of coroutines, flows and channels. Its architecture was inspired by a talk at KotlinConf 2018. Here is a link to TileCollector.kt, the “engine” of this library. Here’s a diagram:

The point of this architecture is to never block the UI thread while fetching and manipulating tiles. However, TileCollector is just one piece of the machinery. There’s also the logic of evicting tiles which aren’t visible anymore. That logic isn’t represented in the diagram. If you’re curious, have a look at TileCanvasState.

Available on MavenCentral

To start using MapCompose, just add this to your build.gradle

implementation 'ovh.plrapps:mapcompose:1.0.0'

Before Jetpack Compose hit 1.0 (e.g during the beta phase), I experimented different APIs. Now, the API surface is stable.

Conclusion

I’m looking forward to get feedback from the Android community. Go ahead, try MapCompose, and don’t hesitate to share your thoughts on the Github repository, either as an issue or a discussion.