Skip to content

Kotlin Compose Multiplatform library for Google Maps on Android and iOS

License

Notifications You must be signed in to change notification settings

yankeppey/kmp-maps-compose

Repository files navigation

Maven Central

Maps Compose Multiplatform

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.

Sample app running on iOS and Android

Installation

Add the dependency to your build.gradle.kts:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("eu.buney.maps:kmp-maps-compose:0.2.0")
        }
    }
}

API Key Setup

Get a Google Maps API key from Google Cloud Console.

Each platform requires the API key to be configured differently:

  • Android: Add a <meta-data> entry in your AndroidManifest.xml
  • iOS: Call GMSServices.provideAPIKey(key) before using maps

Example using BuildKonfig

BuildKonfig 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, "MAPS_API_KEY", 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()
}

iOS SDK Setup

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:

  1. In Xcode, go to File → Add Package Dependencies...
  2. Click Add Local...
  3. Navigate to kmp-maps-compose/exportedGoogleMapsBridge and add it

This 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:

  1. In Xcode, go to File → Add Package Dependencies...
  2. Enter: https://github.com/googlemaps/ios-maps-sdk
  3. Select the version matching what the library expects (check exportedGoogleMapsBridge/Package.swift for the exact version)

Note: With this approach, you're responsible for keeping the SDK version in sync with library updates.

Quick Start

import eu.buney.maps.*

@Composable
fun MapScreen() {
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition(
            target = LatLng(37.7749, -122.4194), // San Francisco
            zoom = 12f
        )
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        properties = MapProperties(mapType = MapType.NORMAL),
        uiSettings = MapUiSettings(zoomControlsEnabled = true),
        onMapClick = { latLng -> println("Clicked: $latLng") }
    ) {
        Marker(
            state = rememberUpdatedMarkerState(position = LatLng(37.7749, -122.4194)),
            title = "San Francisco",
            snippet = "Welcome!"
        )
    }
}

Features

Map Display

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.
}

Markers

// basic marker
Marker(
    state = rememberUpdatedMarkerState(position = LatLng(37.7749, -122.4194)),
    title = "Title",
    snippet = "Description",
    alpha = 0.8f,
    rotation = 45f,
    draggable = true,
    onClick = { marker -> false } // return true to consume
)

// custom info window
MarkerInfoWindow(
    state = rememberUpdatedMarkerState(position = latLng),
    onClick = { false }
) { marker ->
    Column(
        modifier = Modifier.background(Color.White).padding(8.dp)
    ) {
        Text("Custom Window", fontWeight = FontWeight.Bold)
        Text("Any Compose content!")
    }
}

// compose content as marker icon
MarkerComposable(
    keys = arrayOf(count),
    state = rememberUpdatedMarkerState(position = latLng)
) {
    Box(
        modifier = Modifier
            .size(40.dp)
            .background(Color.Red, CircleShape),
        contentAlignment = Alignment.Center
    ) {
        Text("$count", color = Color.White)
    }
}

Shapes

// 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
)

Camera Control

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.animateToBounds(bounds, padding = 100)
}

// instant move
cameraPositionState.move(CameraPosition(target = latLng, zoom = 10f))

// check camera state
if (cameraPositionState.isMoving) {
    // camera is animating or user is panning
}

Custom Marker Icons

// 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
)

Feature Parity with android-maps-compose

This table shows feature compatibility between android-maps-compose and this library.

Core Map Features

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 No
latLngBoundsForCameraTarget Yes No
MapColorScheme Yes No
LocationSource Yes No

Camera

Feature android-maps-compose kmp-maps-compose Notes
CameraPositionState Yes Yes
CameraPosition (target, zoom, bearing, tilt) Yes Yes
animate() Yes Yes
animateToBounds() Yes Yes
move() Yes Yes
isMoving Yes Yes
cameraMoveStartedReason Yes Yes
projection Yes Yes
visibleBounds Yes Yes

Markers

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

Shapes & Overlays

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 No

Advanced Features

Feature android-maps-compose kmp-maps-compose Notes
StreetView Yes No
Clustering Yes No
ScaleBar widget Yes No
MapEffect Yes No
IndoorStateChangeListener Yes No

Legend: Yes = Supported | Partial = See notes | No = Not supported

iOS-Specific Notes

  • Uses Google Maps iOS SDK via Swift Package Manager
  • Some styling features not supported by iOS SDK:
    • Polyline/polygon stroke patterns
    • Line caps and joint types
    • Map toolbar and zoom controls
  • TERRAIN map type falls back to NORMAL
  • Z-index values are truncated to integers
  • Custom info window Compose content rendered as bitmap

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

About

Kotlin Compose Multiplatform library for Google Maps on Android and iOS

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published