Let's Work Together
April 2, 2025 · 15 min read

Compose Multiplatform: A Practically-Native Example!

Building a Compose Multiplatform Composable using Native Components

Author Jonathan Davis
Compose Multiplatform: A Practically-Native Example!

What do you do when you really want to multi-platform but there isn’t an out-of-the-box CMP (Compose Multiplatform) Composable available? You build it yourself of course! But then you start thinking to yourself, “That’s easier said than done”. Whipping up a Composable using the Compose primitives such as Box, Column, Row etc is trivial. What are you to do when you need something tricky, dynamic, and with tons of complexities such as an interactive map?

You start internally panicking and wondering where do you even start? How do you draw Markers, Polylines, terrains and buildings. Heck, how do you handle the Map camera that can be zoomed, rotated and animated? Google, Apple, and MapBox most certainly have dedicated teams with years of experience to lean on in order to build such a component. How are you, a lone developer, going to solve this? The gravity of the task at hand starts to set in, and you begin to feel this task is insurmountable…

Then you remember what you just said, this solution has been solved by existing companies. Companies who’s solutions have been battle tested over several years with new additions being added (Google Maps got a new default renderer back in 2025). The 💡 goes off in your head and you realize that you don’t need to rebuild the wheel, you just need to integrate it in your metaphorical car. This is where a core strength of Compose comes into play. It doesn’t only allow you to declaratively build a UI that targets Android and iOS, it has built-in support for inter-opting with native platform UI elements ie Views & Composables (duh!) as well as UIView & SwiftUI.

Getting Started?

Before you can get cracking on some code, you need to ensure your development environment is setup. JetBrains offers fantastic documentation here to get you started.

Once you’ve setup your environment use the Kotlin Multiplatform Wizard found to create your project that targets Android & iOS.

Kotlin Multiplatform Wizard

Hit “Download” and open the project in Android Studio.

Google Maps SDK Setup?

Next, we need a Google Cloud Project so that we can get an API Key to access the Google Maps API. Check out this official guide.

Google Maps API Key?

Now that you have a project setup, we need our API Key. Lucky for us (me) there is another official guide to do this. It’s not necessary to restrict your API key to follow along but in a real world applications you ⚠️100%⚠️ should restrict your key.

Open strings.xml from the androidMain source set and add an entry named gmaps_api with the API key your generated above as the value. Next, open AndroidManifest.xml and added the following into the application block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Material.Light.NoActionBar">
        <!--Declare GMaps API key so that the Google Maps library can automatically 
        extract it when initializing the Map-->
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="@string/gmaps_api" />
        <!--Define the GPS the app depends on-->
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        
        <!--other entries omitted-->
</application>

Native View Interop?

We’re going to build a real CMP interactive Map Composable utilizing native Google Maps SDK on both Android and iOS. Now you may be thinking wouldn’t MapKit work for this very situation? Why do we need to use Google Maps? Apples is better blah, blah. We are going to be using Google Maps SDK on iOS as well instead of MapKit because there will be consistency in features and map appearance with Android. If in the future we decide to use Cloud-based map styling, we don’t need to do any additional work to get a matching theme on iOS.


View Interop (Android)

So how exactly, do you host a View in the Jetpack Compose? You wrap it in a Composable 😁, of course. The fantastically named AndroidView(...) composable is your bridge between Compose and Views. Just return your View from the factory lambda and like magic you’re composing with the best of ’em.

Here is an excerpt comes from AndroidView(...) documentation:

“AndroidView is commonly needed for using Views that are infeasible to be reimplemented in Compose and there is no corresponding Compose API. Common examples for the moment are WebView, SurfaceView, AdView, etc.”

This is exactly what we need to reuse Google Maps MapView in a Compose app. Conveniently, the Android Google Maps teams have released Compose library that wraps MapView in a Composable and provides state classes for managing the Map from Compose. While this saves us some work, it does feel like a bit of a cheat so I will at least show you a snippet of the GoogleMaps() Composable so that you can see AndroidView in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Composable
public fun GoogleMap(
    /* Code omitted for readability */
    ...
) {
    // When in preview, early return a Box with the received modifier preserving
    // layout
    if (LocalInspectionMode.current) {
        Box(modifier = modifier)
        return
    }

    val context = LocalContext.current
    val mapView = remember { MapView(context, googleMapOptionsFactory()) }

    AndroidView(modifier = modifier, factory = { mapView })
    MapLifecycle(mapView)

    /* Code omitted for readability*/
  ...
}

UIKit Interop (iOS)

All we need to do is host a UIView in in a Composable, similar to what we did with Android (minus the AndroidView). This doesn’t seem so hard, and really it’s not. Similar to Android, Compose Multiplatform offers the UIKitView(factor = {...}) composable to handle inter-op with UIKit. You create your UIKit view and just return it in the factory lambda and boom more magic, you’re composing on your iPhone.

This is where the tricky part comes in. While you can directly instantiate UIViews that are part of the UIKit package via the platform.UIKit import from your Kotlin code (in iosMain source set) we cannot import third-party code. We have no way of directly calling any code from the Google Maps SDK from the iosMain source set.

We have two options:

  1. Use use CocoaPods 🤮 to depend on the GoogleMaps library
    • Allows you to directly import third-party dependencies by prefixing the package with with cocoapods.*
    1
    
    import cocoapods.GoogleMaps.GMSMapView
    
    • But the trade off is you have to use CocoaPods which has fallen out of favor for Swift Package Manager (SPM)
    • Requires you to run the iOS app from Xcode 🤮.
    • Random build errors pop up with dependencies ❌.
  2. Use SPM 🤩 and leverage Interfaces & DI
    • Define an Kotlin Interface that Swift/Objective-C code will implements to instantiate the GMSMapView and DI (Koin) will inject this interface in your Kotlin code to be displayed in a UIKitView(factor = {...}).
    • Can build & run iOS from Android Studio ✅.

Cocoapods seems to have fallen out of favor for Apples official package manage solution, SPM. Make the most sense to me to stack with officially supported technology, so we’re going with option 2. Also being able to run your app from Android Studio is a tiny bonus; its the little things in life.

Project Setup

Before we can get started, we need to add some version catalog entries to a few libraries that will make this whole thing possible.
Open gradle/libs.versions.toml and add the following to the end of the [versions] section:

1
2
playServiceMaps = "19.1.0"
koinCore = "4.0.4"

Add the following to the end of the [libraries] section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Google Maps (Android only)
maps-compose = { 
    module = "com.google.maps.android:maps-compose",
    version = "mapsCompose" 
}
maps-compose-utils = { 
    module = "com.google.maps.android:maps-compose-utils",
    version.ref = "mapsCompose" 
}
# Google Maps relies on Play services (Android only)
play-services-maps = { 
    module = "com.google.android.gms:play-services-maps",
    version.ref = "playServiceMaps" 
}
# Koin
koin-core = { 
    module = "io.insert-koin:koin-core",
    version.ref = "koinCore" 
}
koin-compose = { 
    module = "io.insert-koin:koin-compose",
    version.ref = "koinCore" 
}

Sync the gradle project then open composeApp/build.gradle.kts. We need to add the following version catalog entries declarations to the end of androidMain.dependencies {} block.

1
2
3
4
// This contains the Google Maps compsoable
implementation(libs.maps.compose)
implementation(libs.maps.compose.utils)
implementation(libs.play.services.maps)

Lastly, add the following declarations to the end of commonMain.dependencies {} block. This will be necessary to get the iOS App working.

1
2
3
4
// Dependency injection is need to get iOS side of things working
api(libs.koin.core)
// Allow DI from composable code
api(libs.koin.compose)

Perform another project sync. Now your android code (androidMain source) is able to call the Google Maps Compose API.

iOS

Find the .xcodeproj file in the /iOSApp directory:

iOSApp Folder structure
Like Android, we need to add the Google Maps SDK dependency to the iOS project.

Open the Swift Package Manager through the xcode menu options (File/Add Package Dependencies…). In the SPM tool window, enter the following Google Maps SDK into the search field: https://github.com/googlemaps/ios-maps-sdk.
You should see a single search result like below:

Swift Package Manager - SPM Google Maps SDK

Select “Add Package” and you’re good to go!

KMP expect/actual - How it’s done

Now that we know how to display legacy views in Compose, how do we call the appropriate code on Android/iOS? In comes Kotlin Multiplatform’s Expected and Actual declarations to the rescue. I encourage you to take a look at the documentation for the feature here. The TLDR is inside of the “common” source set (composeApp/commonMain) you abstractly define functionality (classes and functions) using the keyword expect which essentially defers the implementation to platform specific target source sets (ie composeApp/androidMain, composeApp/iosMain in our case) using the actual keyword. The compiler will guarantee there is a matching expect declaration in each target platform-specific source set before you can build it. Also, the compiler will make sure you only call APIs available on the respective platform ie you cannot use java.io.File in any non-JVM source set.

commonMain - expect

Enough yap! You’re ready to build your first multiplatform Composable. In the commonMain source set, create a new Kotlin file called GMap.kt in the composeApp/commonMain directory then paste the following code:

1
2
3
4
@Composable
expect fun GMap(
    modifier: Modifier = Modifier,
)

Since there are no actual declarations for the GMap() in each target source set, you’ll get a compilation error at the expect keyword:

Missing actual declarations

From the tooltop window select “More Options”:

Add actual declarations
Select the androidMain and iosMain options; iosMain encompasses x64Main, Arm64Main, and SimulatorArm64Main. Android Studio will create a new file GMap.kt file in the appropriate platform source set.
Add actual declarations

androidMain - actual

Your Gmaps.android.kt should look like the following:

Empty Android GMap actual declarations

Now, all we need to do is call the display the map. Replace the contents of the file with the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.google.maps.android.compose.GoogleMap

@Composable
actual fun GMap(
    modifier: Modifier,
) {
    // Here, we have access to the Android dependencies
    // We want to use the GoogleMaps() from Google-Maps-Compose lib...
    GoogleMap(
        modifier = modifier,
    )
}

For our first actual implementation, we will proxy to the GoogleMaps() composable. Which behind the scenes is really just using the AndroidView(...) to wrap MapView. Can’t get much simpler than this.


Before we can try running the Android app, we need to actually call our new composable. Open App.kt, and replace the contents with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jetbrains.compose.ui.tooling.preview.Preview

@Composable
@Preview
fun App() {
    MaterialTheme {
        Box {
            // Display our Map composable fullscreen
            GMap(
                modifier = Modifier.fillMaxSize(),
            )
        }
    }
}

Make sure composeApp run configurations is selected is hit run:

Android Run Configuration

You should see something that looks like this:

GMaps Composable on Android emulator


iosMain - actual

Like Android, the iOS GMaps actual file should very similar:

Empty iOS GMap actual declarations


Unlike Android, the iOS implementation requires a more “colorful” solution. Since we cannot import and instantiate the GMSMapView() from Kotlin code, What are we to do?

Since our Kotlin code cannot access the GMSMapView() we must do it from Swift code. But then you think, wait. Kotlin cannot directly interop with Swift code, this won’t work either. But slow your roll, don’t get ahead of yourself and throw the baby out with the bathwater. While we cannot call Swift code from Kotlin, Swift can implement Kotlin interfaces. That gives us something to work with.

All we need to do is define an interface that will return a UIVIew (GMSMapView() in our case) and implement it in Swift. Then we just need a way to provide that Swift backed interface to our Kotlin code in iosMain source set but we’ll cover that shortly. We now have the vision, we just need to act on it.
In the iosMain source set add a new Kotlin file called IGMapsBridge.kt and paste the following code:

1
2
3
4
5
6
/**
 * We will implement this in Swift and return a GMSMapView.
 */
interface IGMapsBridge {
    fun mapView(): UIView
}

Now we just need to implement the interface. In the iosApp project folder, add a new Swift file called IOSGMapsBridge.swift. Copy & Paste the code below then build the xcode project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import ComposeApp
import UIKit
import GoogleMaps

class IOSGMapsBridge : IGMapsBridge {
    /*
        Instead of creating a new instance on every mapView() call we want this 
        to be a non-null class level variable so if we ever want to we can 
        mutate the map state.
     */
    private let _mapView: GMSMapView

    init() {
        self._mapView = GMSMapView()
    }
    /**
     * Return a full constructed map view 
     */
    func mapView() -> UIView { _mapView }
}

If you get a “No such Module ‘ComposeApp’” build errors try performing a gradle sync and rebuild xcode project. There can be a bit of friction at this juncture but remember iOS support is still in beta.


That’s cool and all but aren’t we back where we started? We’ve got more Swift code we can’t instantiate. Trust the process. Remember me mentioning something about Dependency Injection (DI) and that Koin dependency I had you include early? Well it’s finally coming into play. We’re going to instantiate IOSGMapsBridge from the Swift side then we will us Koin so we can inject it into Kotlin code. Creative, right?

In the iosMain source set, create a new file called PlatformDI.kt and paste the following code to setup Koin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.atomicrobot.cmmaps

import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.core.module.Module
import org.koin.dsl.bind
import org.koin.dsl.module

/**
 * Start the Koin Application
 */
fun initDI(
    extraModules: Array<Module> = emptyArray()
): KoinApplication {
    stopKoin()
    return startKoin {
        modules(*extraModules)
    }
}


/**
 * Initializes Koin with a IGMapsBridge factory lambda.
 */
fun loadKoinSwiftModules(
    gmapsBridgeFactory: () -> IGMapsBridge,
) {
    initDI(
        arrayOf(
            module {
                /*
                    Register the lambda as factory def, so if dependent code ever 
                    needs it they get a fresh instance of the IGMapsBridge. This is
                    especially important if the implementation IGMapsBridge caches 
                    an instance of the GMSMapView.
                /*
                factory {
                    gmapsBridgeFactory()
                } bind IGMapsBridge::class
            },
        )
    )
}

The code is simple enough, but the tldr: We are setting up Koin to register a “factory” lambda that will instantiate our IGMapsBridge interface. Now, we just need to provide factory lambda from our Swift code. Preferrably at app launch. In iOSApp.swift, we’re going to add an initializer that we will call loadKoinSwiftModules(...) passing in a lambda that instantiates IOSGMapsBridge. We also want to initialize the GMSServices using the API key you generated earlier. Again, you should not directly store your API key in a production app 😬.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import SwiftUI
import GoogleMaps
import ComposeApp

@main
struct iOSApp: App {
    // DO NOT DO THIS IN PRODUCTION... 
    // Follow recommended practice for storing/accessing API keys on iOS.
    let gmapsApiKey: String = "<API-KEY>"
    
    init() {
        // Initialize Google Maps Service otherwise the app will crash on runtime
        GMSServices.provideAPIKey(gmapsApiKey)
        // Initialize Koin with out GMapsBridge factory that returns new instances
        // of IOSGMapsBridge.
        ComposeApp.PlatformDIKt.loadKoinSwiftModules {
            IOSGMapsBridge()
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Now that we’ve gotten the interface implemented and Koin setup, we can finally implement the GMaps() composable. Replace the body of GMaps() with the function below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Composable
actual fun GMap(
    modifier: Modifier,
) {
    // Fetch the injected IGMapsBridge Interface
    val mapBridge = koinInject<IGMapsBridge> ()
    val map = remember { mapBridge.mapView() }
    UIKitView<UIView>(
        factory = { map },
        modifier = modifier,
        properties = UIKitInteropProperties(
            interactionMode = UIKitInteropInteractionMode.NonCooperative
        ),
    )
}

Make sure iosApp run configurations is selected is hit run:

iOSMain Run Configuration

It may take a moment to fully launch your app on the simulator but if you’ve setup everything correct, you should see the following:

GMaps Composable on iOS simulator

Conclusion

Using some ingenuity, we were able to create our own Google Maps Multiplatform Composable using pure Kotlin & Swift. While our example may be basic we learned some advanced concepts such as dependency inject, implementing a Kotlin interface in Swift, and using Expected and Actual declarations. This architecture can be extended to implement several features of the Google Maps API, such as setting the map’s initial camera position, and adding Markers/Polylines.

Compose Multiplatform is a powerful framework that can take your company to the next level. If you are interested and exploring what’s possible and how going multiplatform with your apps reach out to us at Atomic Robot – we’d be happy to help.

Copyright © 2025 JetBrains s.r.o. Kotlin Multiplatform and Compose Multiplatform and the Kotlin Multiplatform and Compose Multiplatform logos are trademarks of JetBrains s.r.o. www.jetbrains.com

Copyright © 2025 Google LLC. Jetpack Compose, Android, and Google Maps logos are trademarks of Google LLC. www.google.com