Interoperability and Compose API lessons learned while building Maps Compose
This year we released the Maps Compose library, a Jetpack Compose library for adding Google Maps to your app. At its core, Maps Compose is an interop library for the Maps SDK for Android that exposes Compose friendly APIs. So, to create the composable elements of Maps Compose we needed to work under the constraints of the existing API of the Maps SDK, and the available Compose interop functions.
Maps Compose went through a couple of design and implementation iterations before eventually landing the initial release. In this blog post, I’d like to cover the background around Maps Compose, how it came to be, and some lessons learned while working on it. This post is particularly relevant to SDK developers wanting to provide Compose support; however, it should also be relevant for the Compose-curious readers out there. If you fall into one of those buckets — read on!
From our partners:
Before Maps Compose was available, using the Maps SDK in Compose was already possible thanks to Compose’s interoperability APIs, namely, the AndroidView composable. Simple use cases — like showing a map with a single marker on it — were straightforward; however, moderately complex integrations with lots of customizations and drawings required writing a lot of interop code, making usage not as straightforward.
To get a gauge on how useful a Compose library might be for Android developers, I tweeted the following:
To my surprise, a lot of developers responded with statements like “Yes, please!”, “Cool!”, and “This would replace a few hundred lines of code.” 🤯 As one of the Developer Relations Engineers working on the Maps SDK at the time, it made sense to prioritize releasing composable elements for the Maps SDK. From there, I internally proposed a design and implementation for Maps Compose and collaborated with Adam Powell, Leland Richardson, and Ben Trengrove to get it to the finish line.
Giving feedback makes a difference and translates to real product changes. We love hearing from you!
Lesson #1: Reuse classes in the core Maps SDK
The Maps SDK has been around for 10+ years and is used by many apps. While Compose introduces a completely different way of building UI, a composable version of maps should still feel familiar. If you know a bit of Compose, and have used the Maps SDK before, composable maps should be intuitive for you.
To help make the API intuitive, we reused the underlying Maps SDK classes when possible. For example, to perform camera updates in Maps Compose, you can still use the existing CameraUpdate class created from the CameraUpdateFactory object.
There were some instances, however, when the existing classes in the Maps SDK couldn’t be used as-is. For example, the UiSettings class did not make sense to be reused since it can only be retrieved once the map has been created. Ideally, you should be able to create an instance of this class and be able to pass it into the GoogleMap composable. To work around this, the class was mirrored to a Maps Compose type, MapUiSettings. The naming closely matches the existing UiSettings class with the additional “Map” prefix to help with discoverability of the API. MapUiSettings has the same exact properties and default values as UiSettings, the difference being that it can be created and passed into the GoogleMap composable as opposed to obtaining and mutating it from another control surface:
There are other properties of the map that can be changed at runtime (for example, setBuildingsEnabled(boolean)). One consideration we had was to expose these properties as individual parameters on the GoogleMap composable; however, that would significantly increase the number of parameters since there are a lot of properties that can be toggled. Instead, we opted to create a separate class, MapProperties, which contains these runtime-configured properties:
For objects that can be drawn on the map (markers, polylines, etc.), the imperative View-based approach is to call add* methods like GoogleMap.addMarker(MarkerOptions) on the GoogleMap object. To convert this in Compose, the GoogleMap composable could accept a list parameter for each drawing, however, this API could be difficult to use for integrations that contain a lot of drawn objects with complex logic. Instead, we decided to expose a Compose friendly API — a generic content lambda wherein drawings can be invoked as separate composables.
Need to draw a marker, polyline, or other supported drawn object on the map? Call the Marker, Polyline, or another decorator composable function in the content lambda of the GoogleMap composable like so:
Lesson #2: Take advantage of Kotlin features
The Maps SDK was built at a time before Kotlin was a first-class language for writing Android apps. That said, the Maps SDK is largely built in Java. Jetpack Compose on the other hand, is entirely written in Kotlin and heavily leans on Kotlin idioms. For Maps Compose, we also decided to lean in on Kotlin language features like coroutines to expose a Kotlin idiomatic API.
For instance, Compose uses Kotlin’s coroutine suspending functions for animation APIs. So, it made sense to provide a similar API for the underlying callback-based APIs in the Maps SDK. For example, animating the camera and waiting for it to complete can be done within a coroutine scope:
For a list of other Kotlin idioms widely used in Compose, see Kotlin for Jetpack Compose.
Lesson #3: Maintain consistency with other Compose toolkit APIs
Maintaining consistency with other Compose toolkit APIs enables good developer ergonomics. Doing so allows functions to be easier to use, and thus faster to develop with, as it follows a familiar convention with other APIs. Whether you are a library or app developer, this consistency is essential to promote ease-of-use. The Compose API guidelines is an excellent resource for learning conventions followed by Compose APIs.
Here are a few patterns outlined in the guidelines that Maps Compose adopts:
- The GoogleMap composable complies with Elements accept and respect a Modifier parameter.
- The MarkerInfoWindow composable complies with this point listed under Compose UI Layouts: “Layout functions should place their primary or most common @Composable function parameter [this should be named content] in the last position to permit the use of Kotlin’s trailing lambda syntax”.
There have been a couple of instances where my initial designs differed from these guidelines, and I found it very helpful to refer to them to adjust the API decisions to better align with Compose best practices.
Lesson #4: Plain classes are best for binary compatibility
Kotlin data classes are an efficient way to hold data. They offer a handful of generated methods that you don’t have to write yourself, saving you several lines of code per class. However, if you’re writing a library, data classes have a hidden cost as future changes to the data class break binary compatibility. Adding new properties will change the generated method signature for copy(), and depending on where the new property was added, it could also break destructuring functions thereby breaking consumers. To mitigate this, Maps Compose uses plain classes for MapUiSetting and MapProperties. Hat tip to Jake Wharton for pointing this out in his Public API challenges in Kotlin blog post.
Lesson #5: Use Compose common types
To customize colors of a drawn object in the Maps SDK, you provide it a color integer. For example, to customize a Circle’s fill color you provide it a color integer when constructing the CircleOptions object. The Maps Compose Circle on the other hand, uses the Compose provided Color class instead.
One of the awesome features of Compose is its built-in support for applying material theming to your app. So by using the Compose provided Color class, when the GoogleMap and its children composables are used within a MaterialTheme, the colors automatically adapt to your theme’s colors when the system appearance changes to either light or dark mode.
Lesson #6: Subcompositions are powerful
Early in the development process, we identified that adding and removing map decorations using side effect APIs over time to match the app’s data model was tedious and error prone. The need to manage a tree of elements is effectively the same problem as managing a composable UI tree, and so a better solution would be to use the same underlying tooling for directly updating the elements as state changes over time. This approach turned out to be a lot more straightforward and intuitive over using side effects.
To achieve this, we used the Applier class and the ComposeNode composable to support the child-based API (content lambda) of adding drawn objects (markers, polylines, polygons, etc.) on the map. The resulting implementation creates a new subcomposition that manages the map’s state instead of Compose UI nodes.
Taking a marker as an example, with subcomposition we are able to ensure that the map’s state is updated as recomposition occurs. So for example, if a Marker composable was previously in the composition and was later removed, we can tap into the appropriate node removal method to ensure the underlying Marker Maps SDK object is also removed from the map.
Overall, I was impressed at how the available Compose interop APIs made supporting the Maps SDK in Compose doable. While a native implementation of the Maps SDK is to-be desired, Maps Compose bridges the gap for many Maps developers using Compose.
Hopefully you found this post insightful and learned a thing or two about designing Compose APIs and making Compose work with existing View code.
If you’re new to Compose or Maps Compose, check out the sample apps to learn more:
If there’s anything you’d like to see next, let me know and leave a comment below!
For enquiries, product placements, sponsorships, and collaborations, connect with us at [email protected]. We'd love to hear from you!
Our humans need coffee too! Your support is highly appreciated, thank you!