Migrating Google Maps to Jetpack Compose
By Gemma Lara Savill
Published at February 24, 2025
It is not surprising to find apps with a legacy XML Google Maps implementation.
Changing to the newer Compose Google Maps library is not straightforward, but very rewarding. Also, this a step to future-proof your app, reduce technical debt and improve your code testability.
The first major change that stands out is the change from a Callback to a Compose state data observation-update lifecycle.
In my experience, it is hardly ever about just a map; it is usually a map with many other components overlaid and a lot of data flowing in and out. Changing to a Compose Map enables us to free the Fragment or any other class used for callback implementation and control all state through the ViewModel. This gives us the benefit of controlling our UI state within the Android lifecycle. Also, we can now create a single state for the screen that combines map-related and non-map-related data, such as data displayed via composables overlaid on the map, like custom menus and search boxes.
A look at the Google Maps composable code
The Google Maps composable brings a great many helpful functions and parameters.
@Composable
fun GoogleMap(
modifier: Modifier = Modifier,
mergeDescendants: Boolean = false,
cameraPositionState: CameraPositionState = rememberCameraPositionState(),
contentDescription: String? = null,
googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() },
properties: MapProperties = DefaultMapProperties,
locationSource: LocationSource? = null,
uiSettings: MapUiSettings = DefaultMapUiSettings,
indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener,
onMapClick: (LatLng) -> Unit? = null,
onMapLongClick: (LatLng) -> Unit? = null,
onMapLoaded: () -> Unit? = null,
onMyLocationButtonClick: () -> Boolean? = null,
onMyLocationClick: (Location) -> Unit? = null,
onPOIClick: (PointOfInterest) -> Unit? = null,
contentPadding: PaddingValues = DefaultMapContentPadding,
mapColorScheme: ComposeMapColorScheme? = null,
content: @Composable () -> Unit = {}
)
You can configure your map's look and feel with the uiSettings parameter
class MapUiSettings(
val compassEnabled: Boolean = true,
val indoorLevelPickerEnabled: Boolean = true,
val mapToolbarEnabled: Boolean = true,
val myLocationButtonEnabled: Boolean = true,
val rotationGesturesEnabled: Boolean = true,
val scrollGesturesEnabled: Boolean = true,
val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true,
val tiltGesturesEnabled: Boolean = true,
val zoomControlsEnabled: Boolean = true,
val zoomGesturesEnabled: Boolean = true
)
To disable zoom controls, simply set
GoogleMap(
...
uiSettings = MapUiSettings(zoomControlsEnabled = false),
...
)
Or allow your users to configure the look and feel of their map, and pass the values through the ViewModel as State.
// In your ViewModel
var mapUiSettings by mutableStateOf(MapUiSettings(zoomControlsEnabled = false))
// In your Composable
GoogleMap(uiSettings = mapUiSettings)
And you can configure your map even further via the the MapProperties parameters.
class MapProperties(
val isBuildingEnabled: Boolean = false,
val isIndoorEnabled: Boolean = false,
val isMyLocationEnabled: Boolean = false,
val isTrafficEnabled: Boolean = false,
val latLngBoundsForCameraTarget: LatLngBounds? = null,
val mapStyleOptions: MapStyleOptions? = null,
val mapType: MapType = MapType.NORMAL,
val maxZoomPreference: Float = 21.0f,
val minZoomPreference: Float = 3.0f
)
For example, to enable traffic, set
GoogleMap(
properties = MapProperties(isTrafficEnabled = true)
)
All easy to configure, as most of these are simple Booleans. And of course you can pass in the value for any of these parameters via your state.
Adding interactivity to Google Map
You do this via the functions, like
onPOIClick: (PointOfInterest) -> Unit? = null,
onMyLocationButtonClick: () -> Boolean? = null,
You can leverage these and update the content within the map with markers and routes. For example:
// Example of adding a custom marker to the map.
@Composable
fun CustomMarkerMap() {
var markerPosition by remember { mutableStateOf<LatLng?>(null) }
GoogleMap(
onMapClick = { latLng ->
markerPosition = latLng
}
) {
markerPosition?.let { position ->
Marker(state = MarkerState(position = position))
}
}
}
I also appreciate how Compose Maps allows you to keep all map-related code together, enhancing code discoverability and encapsulation. This includes functions that control the map's look and feel, such as displaying different marker types, adjusting zoom speed, and drawing routes.
Displaying a route on a Compose map is straightforward: simply maintain a list of latitude and longitude points in your state. When the list is not empty, your map composable can render the route, automatically updating as the state changes and the screen recomposes.
// Example of adding a route to the map.
@Composable
fun ExampleRouteMap() {
val exampleRoute = remember {
listOf(
LatLng(51.514760, -0.127890),
LatLng(51.5074, 0.0),
LatLng(51.5152, -0.1639),
)
}
</p><p> GoogleMap {
Polyline(points = exampleRoute)
}
}
Controlling your map through a ViewModel-managed state significantly improves testability, enabling me to practice Test-Driven Development (TDD) effectively. You can test all map-related and non-map-related state interactions within a single test.
For example:
- 'When the user taps the 'Find Nearby Restaurants' button, the map should center on the user's current location and display restaurant markers.'
- 'When the user enters 'London' in the search bar, the search text should display 'London, UK' and the map should move to London.'
I found Compose tests to be valuable for verifying UI actions and state-triggered updates, including the visibility of Compose components overlaid on the map, through the use of test tags.
You can also add screenshot testing to your testing strategy to focus on state UI interactions and the visual appearance of your screens.
Here is a great codelab to get you started
https://developers.google.com/codelabs/maps-platform/maps-platform-101-compose#0