kmp-maps-compose
0.6.0indexedUnified Google Maps Compose API providing map display, markers, shapes, camera control, custom icons, composable info windows, and smooth camera animations.
Unified Google Maps Compose API providing map display, markers, shapes, camera control, custom icons, composable info windows, and smooth camera animations.
A Kotlin Compose Multiplatform library providing Google Maps integration for Android and iOS with a unified API. Designed as a drop-in multiplatform replacement for android-maps-compose.

Add the dependency to your build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("eu.buney.maps:kmp-maps-compose:0.6.0")
// Optional: clustering utilities
implementation("eu.buney.maps:kmp-maps-compose-utils:0.6.0")
}
}
}
Get a Google Maps API key from Google Cloud Console.
Each platform requires the API key to be configured differently:
<meta-data> entry in your AndroidManifest.xmlGMSServices.provideAPIKey(key) before using mapsBuildKonfig is a convenient way to manage secrets in KMP projects. Here's a complete setup:
1. Add BuildKonfig plugin to your app's build.gradle.kts:
plugins {
// ...
id("com.codingfeline.buildkonfig") version "0.15.1"
}
2. Create secrets.properties in your project root (add to .gitignore):
MAPS_API_KEY=your_api_key_here
3. Configure BuildKonfig and Android manifest in your app's build.gradle.kts:
import java.util.Properties
// load secrets
val secretsFile = rootProject.file("secrets.properties")
val secrets = Properties().apply {
if (secretsFile.exists()) load(secretsFile.inputStream())
}
val mapsApiKey = secrets.getProperty("MAPS_API_KEY", "")
android {
defaultConfig {
// make key available in AndroidManifest.xml as ${MAPS_API_KEY}
manifestPlaceholders["MAPS_API_KEY"] = mapsApiKey
}
}
buildkonfig {
packageName = "com.example.myapp"
defaultConfigs {
buildConfigField(STRING, , mapsApiKey)
}
}
4. Add to AndroidManifest.xml:
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
</application>
5. Initialize iOS in your MainViewController.kt:
import GoogleMaps.GMSServices
fun MainViewController() = ComposeUIViewController(
configure = {
GMSServices.provideAPIKey(BuildKonfig.MAPS_API_KEY)
}
) {
App()
}
The iOS app requires the Google Maps iOS SDK to be linked. There are two ways to set this up:
Option 1: Use the exported bridge package (Recommended)
Add kmp-maps-compose/exportedGoogleMapsBridge as a local Swift Package in your Xcode project:
kmp-maps-compose/exportedGoogleMapsBridge and add itThis bridge package is auto-generated by the spm4kmp plugin and ensures version consistency between the SDK version expected by the library and what gets linked into your app.
Option 2: Add Google Maps iOS SDK directly
Add the SDK via SPM in your Xcode project:
https://github.com/googlemaps/ios-maps-sdkexportedGoogleMapsBridge/Package.swift for the exact version)Note: With this approach, you're responsible for keeping the SDK version in sync with library updates.
import eu.buney.maps.*
@Composable
fun MapScreen() {
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition(
target = LatLng(, -),
zoom =
)
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
properties = MapProperties(mapType = MapType.NORMAL),
uiSettings = MapUiSettings(zoomControlsEnabled = ),
onMapClick = { latLng -> println() }
) {
Marker(
state = rememberUpdatedMarkerState(position = LatLng(, -)),
title = ,
snippet =
)
}
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = rememberCameraPositionState(),
properties = MapProperties(
mapType = MapType.HYBRID,
isTrafficEnabled = true,
isBuildingEnabled = true
),
uiSettings = MapUiSettings(
compassEnabled = true,
rotationGesturesEnabled = true
),
contentPadding = PaddingValues(bottom = 80.dp),
onMapClick = { latLng -> /* handle click */ },
onMapLongClick = { latLng -> /* handle long click */ },
onPOIClick = { poi -> /* handle POI click */ },
onMapLoaded = { /* map ready */ }
) {
// add markers, polylines, etc.
}
// basic marker
Marker(
state = rememberUpdatedMarkerState(position = LatLng(, -)),
title = ,
snippet = ,
alpha = ,
rotation = ,
draggable = ,
onClick = { marker -> }
)
MarkerInfoWindow(
state = rememberUpdatedMarkerState(position = latLng),
onClick = { }
) { marker ->
Column(
modifier = Modifier.background(Color.White).padding(dp)
) {
Text(, fontWeight = FontWeight.Bold)
Text()
}
}
MarkerComposable(
keys = arrayOf(count),
state = rememberUpdatedMarkerState(position = latLng)
) {
Box(
modifier = Modifier
.size(dp)
.background(Color.Red, CircleShape),
contentAlignment = Alignment.Center
) {
Text(, color = Color.White)
}
}
val cameraPositionState = rememberCameraPositionState()
// animate to position
LaunchedEffect(targetLocation) {
cameraPositionState.animate(
CameraPosition(target = targetLocation, zoom = 15f),
durationMs = 1000
)
}
// animate to bounds
LaunchedEffect(markers) {
val bounds = LatLngBounds.Builder().apply {
markers.forEach { include(it.position) }
}.build()
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, 100))
}
// instant move
cameraPositionState.move(CameraUpdateFactory.newCameraPosition(CameraPosition(target = latLng, zoom = )))
(cameraPositionState.isMoving) {
}
// from Compose Resources (recommended)
val icon = rememberBitmapDescriptor(Res.drawable.my_marker)
// from PNG/JPEG bytes
val icon = BitmapDescriptorFactory.fromEncodedImage(imageBytes)
// from Compose content (rendered to bitmap)
val icon = rememberComposeBitmapDescriptor(key1, key2) {
Icon(Icons.Default.Place, contentDescription = null, tint = Color.Red)
}
Marker(
state = markerState,
icon = icon
)
This table shows feature compatibility between android-maps-compose and this library.
Legend: Yes = Supported | Partial = See notes | No = Not supported
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
// simple polyline
Polyline(
points = listOf(LatLng(37.77, -122.42), LatLng(37.78, -122.41)),
color = Color.Blue,
width = 5f,
geodesic = true
)
// gradient polyline
Polyline(
points = routePoints,
spans = listOf(
StyleSpan.gradient(Color.Green, Color.Yellow, segments = 1.0),
StyleSpan.gradient(Color.Yellow, Color.Red, segments = 1.0),
),
width = 8f
)
// stamped polyline (textured pattern)
val stampImage = rememberBitmapDescriptor(Res.drawable.arrow_stamp)
Polyline(
points = routePoints,
spans = listOf(
StyleSpan(
style = StrokeStyle.SolidColor(Color.Blue),
stampStyle = StampStyle(stampImage),
segments = routePoints.size.toDouble()
)
),
width = 16f
)
// polygon with holes
Polygon(
points = outerPoints,
holes = listOf(holePoints),
fillColor = Color.Red.copy(alpha = 0.3f),
strokeColor = Color.Red,
strokeWidth = 2f
)
// circle
Circle(
center = LatLng(37.7749, -122.4194),
radius = 1000.0, // meters
fillColor = Color.Blue.copy(alpha = 0.2f),
strokeColor = Color.Blue
)
// ground overlay
GroundOverlay(
position = GroundOverlayPosition.create(bounds),
image = BitmapDescriptorFactory.fromEncodedImage(imageBytes),
transparency = 0.3f
)
| Feature | android-maps-compose | kmp-maps-compose | Notes |
|---|
| GoogleMap composable | Yes | Yes | |
| MapProperties | Yes | Yes | |
| MapUiSettings | Yes | Yes | Some controls Android-only |
| MapType (Normal, Satellite, Hybrid, Terrain) | Yes | Yes | Terrain falls back to Normal on iOS |
| Content padding | Yes | Yes | |
| onMapClick / onMapLongClick | Yes | Yes | |
| onPOIClick | Yes | Yes | |
| onMapLoaded | Yes | Yes | |
| mapStyleOptions (custom JSON styling) | Yes | Yes | |
| latLngBoundsForCameraTarget | Yes | No | |
| MapColorScheme | Yes | No | |
| LocationSource | Yes | No |
| Feature | android-maps-compose | kmp-maps-compose | Notes |
|---|
| CameraPositionState | Yes | Yes | |
| CameraPosition (target, zoom, bearing, tilt) | Yes | Yes | |
| animate(CameraUpdate) | Yes | Yes | |
| move(CameraUpdate) | Yes | Yes | |
| CameraUpdateFactory | Yes | Yes | newCameraPosition, newLatLngZoom, newLatLngBounds |
| isMoving | Yes | Yes | |
| cameraMoveStartedReason | Yes | Yes | |
| projection | Yes | Yes |
| Feature | android-maps-compose | kmp-maps-compose | Notes |
|---|
| Marker | Yes | Yes | |
| MarkerInfoWindow | Yes | Yes | |
| MarkerInfoWindowContent | Yes | Yes | |
| MarkerComposable | Yes | Yes | |
| AdvancedMarker (PinConfig) | Yes | No | |
| MarkerState | Yes | Yes | |
| showInfoWindow / hideInfoWindow | Yes | Yes | |
| Draggable markers | Yes | Yes | |
| Custom BitmapDescriptor icons | Yes | Yes | |
| onClick, onInfoWindowClick, etc. | Yes | Yes |
| Feature | android-maps-compose | kmp-maps-compose | Notes |
|---|
| Polyline | Yes | Yes | |
| Polyline pattern/cap/jointType | Yes | Partial | Android only |
| Polygon | Yes | Yes | |
| Polygon holes | Yes | Yes | |
| Polygon pattern/jointType | Yes | Partial | Android only |
| Circle | Yes | Yes | |
| Circle pattern | Yes | Partial | Android only |
| GroundOverlay | Yes | Yes | |
| TileOverlay | Yes | Yes |
| Feature | android-maps-compose | kmp-maps-compose | Notes |
|---|
| StreetView | Yes | No | |
| Clustering | Yes | Yes | Via kmp-maps-compose-utils module |
| ScaleBar widget | Yes | No | |
| MapEffect | Yes | Yes | Block receives platform NativeMap |
| IndoorStateChangeListener | Yes | No |
Surfaced from shared tags and platforms — no rankings paid for.