S. Serkan Cay

Software Engineer, Android

How to Integrate Google Maps in Compose Multiplatform

Google Maps integration in a Compose Multiplatform project allows developers to create cross-platform applications with powerful map functionalities. In this guide, we’ll walk through the integration process step-by-step, including sample code snippets for better understanding.

1. Adding Dependencies

Add the required dependencies for Google Maps. Depending on the platforms you’re targeting, include the following in your files:

For Android

in composeApp

Include the following dependency in your build.gradle.kts file:

build.gradle.kts
sourceSets {
        
    androidMain.dependencies {
        ...// Other dependencies
        implementation("com.google.maps.android:maps-compose:6.1.0")
    }
          
}

Note: Google Maps Compose library has been added for Android native.

For iOS

We’ll use Swift Package Manager to add dependencies on the iOS native side.

in iosApp

Open the iosApp.xcodeproj file with Xcode.

Select File -> Add Package Dependencies

Paste the following URL into the search field in the top right.

Google Maps package URL: https://github.com/googlemaps/ios-maps-sdk

Select Exact Version as the Dependency Rule and enter the latest version. Then continue by clicking Add Package.

Note: For new projects it is recommended to choose Exact Version.

Note: Google Maps library has been added for iOS native.

2. API Key Configuration

You need an API key for Google Maps.

Go to the Google Cloud Console. Select your project or create a new one. Open the APIs & Services section. Click on the Enable APIs and Services button at the top. Then enable the Maps SDK for Android and Maps SDK for iOS. Open the Google Maps Platform product. Generate an API key from the Keys & Credentials section.

For Android

in androidMain

Add the key to your AndroidManifest.xml

AndroidManifest.xml
<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="YOUR_API_KEY" />

For iOS

in iosApp

Configure your key in iOSApp.swift

iOSApp.swift
import SwiftUI
import GoogleMaps

@main
struct iOSApp: App {

    init() {
        GMSServices.provideAPIKey("YOUR_API_KEY")
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

3. Building the MapView

Now let’s create a simple map UI in Compose.

in commonMain

Let’s create a data class that holds latitude and longitude double values.

Coordinate.kt
data class Coordinate(
    val latitude: Double,
    val longitude: Double
)

The following code snippet introduces a cross-platform MapView composable function in Compose Multiplatform. Using the expect keyword, it defines a shared interface for rendering a map, where the actual implementation will differ depending on the platform (e.g., Android or iOS). The function takes a list of Coordinate objects as input, which can be used to display markers on the map. This approach ensures a unified API for map rendering while leveraging platform-specific capabilities:

MapView.kt
import androidx.compose.runtime.Composable

@Composable
expect fun MapView(
    modifier: Modifier = Modifier,
    coordinates: List<Coordinate>,
)

Now we can write our platform specific actual functions.

For Android

in androidMain

Let’s create a MapView.kt file.

Following code defines the Android-specific implementation of the MapView composable, leveraging Google Maps in Compose. It displays a map that dynamically adjusts its camera to fit all provided coordinates using LatLngBounds. For each coordinate, a marker is added to the map with a customizable title. The createBounds helper function calculates the bounds for all markers, ensuring they are visible.

MapView.kt
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState

@Composable
actual fun MapView(
    modifier: Modifier,
    coordinates: List<Coordinate>,
) {

    val cameraPositionState = rememberCameraPositionState()
    val bounds = createBounds(coordinates)

    LaunchedEffect(Unit) {
        cameraPositionState.move(
            update = CameraUpdateFactory.newLatLngBounds(bounds, 100)
        )
    }

    Box(
        modifier = modifier
    ) {
        GoogleMap(
            modifier = Modifier.fillMaxSize(),
            cameraPositionState = cameraPositionState
        ) {
            coordinates.map {
                val markerState = rememberMarkerState(position = LatLng(it.latitude, it.longitude))
                Marker(
                    state = markerState,
                    title = "Android Native Marker",
                )
            }
        }
    }
}

private fun createBounds(coordinates: List<Coordinate>): LatLngBounds {
    val boundsBuilder = LatLngBounds.builder()
    coordinates.forEach {
        boundsBuilder.include(LatLng(it.latitude, it.longitude))
    }
    return boundsBuilder.build()
}

Our integration on the Android side is now complete! Next, we’ll focus on the iOS native side.

For iOS

in iosMain

Let’s edit the MainViewController.kt file as in the code snippet.

Following code sets up the iOS-specific integration for a Compose Multiplatform application. It defines a MainViewController function that links a native iOS UIViewController with a Jetpack Compose UI using ComposeUIViewController. The mapViewController is initialized as a lambda to render a map view based on a list of coordinates. This approach ensures seamless communication between the Compose UI and native iOS components, enabling shared functionality across platforms.

MainViewController.kt
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController

lateinit var mapViewController: (List<Coordinate>) -> UIViewController

fun MainViewController(
    mapUIViewController: (List<Coordinate>) -> UIViewController
) = ComposeUIViewController {
    mapViewController = mapUIViewController
    App()
}

Next, let’s create a file named MapView.kt.

Following code provides the iOS-specific implementation of the MapView composable, using UIKitView to integrate native iOS views within Compose. The UIKitView creates a map view by invoking the mapViewController with the provided coordinates and embeds it into the Compose layout. The UIKitInteropProperties are configured to enable native accessibility and set the interaction mode to NonCooperative, ensuring smooth integration between Compose and UIKit components. This setup allows seamless rendering of native map views while maintaining compatibility with Compose UI elements.

MapView.kt
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode
import androidx.compose.ui.viewinterop.UIKitInteropProperties
import androidx.compose.ui.viewinterop.UIKitView

@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun MapView(
    modifier: Modifier,
    coordinates: List<Coordinate>,
) {
    UIKitView(
        factory = { mapViewController.invoke(coordinates).view },
        modifier = modifier,
        properties = UIKitInteropProperties(
            isNativeAccessibilityEnabled = true,
            interactionMode = UIKitInteropInteractionMode.NonCooperative,
        )
    )
}

in iosApp

Open the iosApp.xcodeproj file with Xcode.

Note: Make all changes to the iosApp module in Xcode. Making changes in a different IDE can cause various problems.

Create a MapView.swift file.

Following code defines the MapView struct for iOS, implementing the UIViewRepresentable protocol to integrate a native GMSMapView (Google Maps) into SwiftUI. The makeUIView function initializes the map and adds markers for each coordinate, displaying their locations with titles. It also uses a GMSMutablePath to calculate the map’s bounds and adjusts the camera to fit all markers with padding. The updateUIView function is included for future updates but is left empty here. This setup enables seamless integration of Google Maps within a SwiftUI interface, complete with dynamic marker rendering and camera adjustments.

MapView.swift
import ComposeApp
import GoogleMaps
import SwiftUI

struct MapView: UIViewRepresentable {
    var coordinates: [Coordinate]

    func makeUIView(context: Context) -> GMSMapView {
        let options = GMSMapViewOptions()
        let path = GMSMutablePath()
        let gmsMapView = GMSMapView(options: options)

        for coordinate in coordinates {
            let marker = GMSMarker()
            let location = CLLocationCoordinate2D(
                latitude: coordinate.latitude, longitude: coordinate.longitude)
            marker.position = location
            marker.title = "iOS Native Marker"
            marker.map = gmsMapView
            path.add(location)
        }

        let bounds = GMSCoordinateBounds(path: path)
        gmsMapView.animate(with: GMSCameraUpdate.fit(bounds, withPadding: 50.0))

        return gmsMapView
    }

    func updateUIView(_ uiView: GMSMapView, context: Context) {}
}

Open the ContentView.swift file and define the mapUIViewController argument as in the following code:

ContentView.swift
import ComposeApp
import SwiftUI
import UIKit

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController(
            mapUIViewController: {
                (coordinates: [Coordinate]) -> UIViewController in
                return UIHostingController(
                    rootView: MapView(coordinates: coordinates))
            }
        )
    }
    
    ...
    
}

struct ContentView: View {
    ...
}

Our integration on both sides is complete! Now let’s get to the fun part.

4. Using the MapView

In our commonMain module we now have a composable function called MapView. Let’s make the following changes in the App.kt file for testing purposes:

Kotlin
@Composable
@Preview
fun App() {
    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            MapView(
                modifier = Modifier.fillMaxSize(),
                coordinates = listOf(
                    Coordinate(latitude = 41.015137, longitude = 28.979530),
                    Coordinate(latitude = 39.925533, longitude = 32.866287),
                    Coordinate(latitude = 38.423733, longitude = 27.142826),
                    Coordinate(latitude = 37.575275, longitude = 36.922821),
                )
            )
        }
    }
}

Let’s run it and see the result:

Conclusion

Compose Multiplatform makes it easier to write cross-platform applications, and Google Maps adds a powerful tool for location-based features. By following the steps outlined above, you can successfully integrate Google Maps into your project.

Let me know if you’d like to dive deeper into any section or need further edits!

The complete source code for this project is available on GitHub: https://github.com/serkancay/googlemaps-integration-cmp

Featured Photo by Aleksejs Bergmanis from Pexels: https://www.pexels.com/photo/aerial-photo-of-buildings-and-roads-681335/

Leave a Reply

Your email address will not be published. Required fields are marked *