https://kotlinlang.org logo
#multiplatform
Title
# multiplatform
m

Mantas Latkauskas

02/26/2024, 2:17 PM
Hi 👋 I've been trying to add GoogleMaps to my KMP (Android/iOS) project and it's been a struggle. I have expected/actual implementations for the map. Android works fine, but I've been trying to add iOS Pod for three days with no success. I've somewhat been following this project https://github.com/realityexpander/FredsRoadtripStoryteller (I can build it fine). I've tried clear caches, pod deintegrate/install many times 😄 The error I'm getting is :
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld invocation reported errors
ld: framework 'GoogleMaps' not found
I can build the iOS project until I add
Copy code
pod("GoogleMaps") {
            version = "8.3.1"
            extraOpts += listOf("-compiler-option", "-fmodules")
}
in my build.gradle.kts (:composeApp) cocoapods {...} block to access
import cocoapods.GoogleMaps...
in my iosMain directory.
🧵 1
Pods-iosApp.debug.xcconfig (to check the framework search pats etc.)
Copy code
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../composeApp/build/cocoapods/framework" "${PODS_ROOT}/GoogleMaps/Base/Frameworks" "${PODS_ROOT}/GoogleMaps/Maps/Frameworks" "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleMaps/Base" "${PODS_XCFRAMEWORKS_BUILD_DIR}/GoogleMaps/Maps"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/GoogleMaps"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"z" -framework "Accelerate" -framework "ComposeApp" -framework "CoreData" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "GLKit" -framework "GoogleMaps" -framework "GoogleMapsBase" -framework "GoogleMapsCore" -framework "ImageIO" -framework "Metal" -framework "OpenGLES" -framework "QuartzCore" -framework "SystemConfiguration" -framework "UIKit"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
PODS_ROOT = ${SRCROOT}/Pods
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
Podfile
Copy code
target 'iosApp' do
  use_frameworks!
  platform :ios, '15.4'  # ios.deploymentTarget
  pod 'composeApp', :path => '../composeApp'
  pod 'GoogleMaps', '8.3.1'
end
build.gradle.kts (:composeApp)
Copy code
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.jetbrainsCompose)
    alias(libs.plugins.serialization)
    alias(libs.plugins.sqldelight)
    alias(libs.plugins.ksp)
    kotlin("native.cocoapods")
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    id("dev.icerock.mobile.multiplatform-resources") // For some reason alias() doesn't work here
}

kotlin {
    // <https://github.com/google/ksp/issues/1569>
    applyDefaultHierarchyTemplate()

    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "1.8"
            }
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
//            isStatic = true // <https://github.com/JetBrains/compose-multiplatform/issues/3386#issuecomment-1656695188>
//            baseName = "ComposeApp"
            freeCompilerArgs += "-Xbinary=bundleId=com.homato.oddspot"
            export(libs.moko.resources)
            export(libs.moko.graphics)
        }
    }

    cocoapods {
        // Required properties
        // Specify the required Pod version here. Otherwise, the Gradle project version is used.
        version = "1.0"
        summary = "Some description for a Kotlin/Native module"
        homepage = "Link to a Kotlin/Native module homepage"
        ios.deploymentTarget = "15.4"
        podfile = project.file("../iosApp/Podfile")
        framework {
            baseName = "ComposeApp"
            isStatic = true
        }

        pod("GoogleMaps") {
            version = "8.3.1"
            extraOpts += listOf("-compiler-option", "-fmodules")
        }
    }

    sourceSets {
        val commonMain by getting {
            kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
        }
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material)
            implementation(compose.ui)
            @OptIn(ExperimentalComposeLibrary::class)
            implementation(compose.components.resources)

            // Koin
            implementation(libs.koin.core)
            implementation(libs.koin.annotations)
            implementation(libs.koin.compose)

            // Ktor
            implementation(libs.ktor.client.core)
            implementation(libs.ktor.client.json)
            implementation(libs.ktor.client.serialization)
            implementation(libs.ktor.serialization.kotlinx.json)
            implementation(libs.ktor.client.logging)
            implementation(libs.ktor.client.encoding)
            implementation(libs.ktor.client.contentNegotiation)
            implementation(libs.ktor.client.contentSerializationKotlinxJson)
            implementation(libs.result)

            // SQLDelight
            implementation(libs.sqlDelightCoroutinesExtensions)

            // Datetime
            implementation(libs.kotlinx.datetime)

            // MOKO
            api(libs.moko.resources)
            api(libs.moko.resources.compose)

            // Voyager
            implementation(libs.voyager.navigator)
            implementation(libs.voyager.screenModel)
            implementation(libs.voyager.bottomSheetNavigator)
            implementation(libs.voyager.tabNavigator)
            implementation(libs.voyager.transitions)
            implementation(libs.voyager.koin)

            // Logging
            implementation(libs.kermit)
            implementation(libs.kermit.crashlytics)

        }
        androidMain.dependencies {
            // UI
            implementation(libs.androidx.activity.compose)
            implementation(libs.compose.ui.tooling.preview)
            implementation(libs.compose.ui.tooling)

            // Data
            implementation(libs.ktor.client.okhttp)
            implementation(libs.sqlDelightAndroidDriver)

            // Google Maps
            implementation(libs.google.play.services.android.location)
            api(libs.google.play.services.maps)  // api means its exposed to the pure-android app (for init)
            // Google maps for Compose for Android
            implementation(libs.google.maps.android.compose)
            // Clustering
            implementation(libs.google.maps.android.compose.utils)
            //Voyager
            implementation(libs.voyager.navigator)
            implementation(libs.voyager.koin)
            implementation(libs.voyager.screenModel)
        }
        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
            implementation(libs.sqlDelightiOSDriver)
            implementation(libs.google.play.services.maps)
            implementation("co.touchlab:stately-common:2.0.5") // <https://github.com/cashapp/sqldelight/issues/4357>
        }
    }
}

android {
    namespace = "com.homato.oddspot"
    compileSdk = libs.versions.android.compileSdk.get().toInt()

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")

    defaultConfig {
        applicationId = "com.homato.oddspot"
        minSdk = libs.versions.android.minSdk.get().toInt()
        targetSdk = libs.versions.android.targetSdk.get().toInt()
        versionCode = 1
        versionName = "1.0"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    dependencies {
        debugImplementation(libs.compose.ui.tooling)
        implementation(libs.koin.core)
        implementation(libs.koin.android)
        implementation(libs.koin.annotations)
//        ksp(libs.koin.ksp.compiler) // It generated duplicate modules for some reason
    }
}

// KSP workaround

dependencies {
    add("kspCommonMainMetadata", "io.insert-koin:koin-ksp-compiler:1.3.0")
    // Add if there is a need for koin annotations in other that commonMain
//    add("kspAndroid", "io.insert-koin:koin-ksp-compiler:1.3.0")
//    add("kspIosX64", "io.insert-koin:koin-ksp-compiler:1.3.0")
//    add("kspIosArm64", "io.insert-koin:koin-ksp-compiler:1.3.0")
//    add("kspIosSimulatorArm64", "io.insert-koin:koin-ksp-compiler:1.3.0")
}
tasks.withType<KotlinCompile<*>>().configureEach {
    if (name != "kspCommonMainKotlinMetadata") {
        dependsOn("kspCommonMainKotlinMetadata")
    }
}
afterEvaluate {
    tasks.filter {
        it.name.contains("SourcesJar", true)
    }.forEach {
        println("SourceJarTask====>${it.name}")
        it.dependsOn("kspCommonMainKotlinMetadata")
    }
}

// SqlDelight config
sqldelight {
    databases {
        create("Database") {
            packageName.set("com.homato.oddspot")
        }
    }
}

// MOKO config
multiplatformResources {
    resourcesPackage.set("com.homato.oddspot")
}

secrets {
    // The plugin defaults to "local.properties"
    propertiesFileName = "secrets.properties"

    // A properties file containing default secret values. This file can be
    // checked in version control.
    defaultPropertiesFileName = "local.properties"
}
libs.versions.toml
Copy code
[versions]
agp = "8.2.2"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
androidx-activityCompose = "1.8.2"
androidx-appcompat = "1.6.1"
androidx-constraintlayout = "2.1.4"
androidx-core-ktx = "1.12.0"
androidx-espresso-core = "3.5.1"
androidx-material = "1.11.0"
androidx-test-junit = "1.1.5"
compose = "1.6.1"
compose-compiler = "1.5.8"
compose-plugin = "1.5.12"
googleMapsIos = "5.3.0"
junit = "4.13.2"
kotlin = "1.9.22"
koin = "3.5.3"
ktor = "2.3.7"
koin-annotations = "1.3.0"
koin-compose = "1.1.2"
moko-resources = "0.24.0-alpha-3"
moko-generator = "0.23.0"
moko-graphics = "0.9.0"
#secrets-gradle-plugin = "2.0.1"
secretsGradlePlugin = "2.0.1"
serialization = "1.6.2"
result = "1.1.18"
sqldelight = "2.0.1"
ksp = "1.9.22-1.0.17"
kotlinx-datetime = "0.5.0"
detekt = "1.23.4"
voyager = "1.0.0"
kermit = "2.0.3"
android-maps-compose = "3.1.0"
android-location = "21.1.0"
google-maps = "18.2.0"
pods-google-maps = "8.3.1"
pods-google-mapsUtils = "4.2.2"


[libraries]

# Testing
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }

# AndroidX
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }

# Compose
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }

# Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" }
koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" }

# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktor" }
ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-contentSerializationKotlinxJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

# Result
result = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "result" }

# SQLDelight
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
sqlDelightAndroidDriver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqlDelightiOSDriver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
sqlDelightCoroutinesExtensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }

# MOKO
moko-resources-generator = { module = "dev.icerock.moko:resources-generator", version.ref = "moko-resources" }
moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" }
moko-resources-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko-resources" }
moko-graphics = { module = "dev.icerock.moko:graphics", version.ref = "moko-graphics" }

# Voyager
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
voyager-bottomSheetNavigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyager" }
voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }

# Logging
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kermit-crashlytics = { module = "co.touchlab:kermit-crashlytics", version.ref = "kermit" }

# Google Maps
google-play-services-android-location = { module = "com.google.android.gms:play-services-location", version.ref = "android-location" }
google-play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "google-maps" }
google-maps-android-compose = { module = "com.google.maps.android:maps-compose", version.ref = "android-maps-compose" }
google-maps-android-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "android-maps-compose" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
#secrets-gradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" }
i

Ismail

02/26/2024, 2:51 PM
I created a small project to showcase how to use Google maps in KMM and to reference it later on if I had any issues implementing it in other projects. https://github.com/ismai117/KMap If it still doesn't recognize the pod, then go into terminal CD iosApp and enter the following command pod update
🙏 1
m

Mantas Latkauskas

02/26/2024, 2:58 PM
Thanks! I see you've got a few things set up differently than I. Will try out setting it up the same way. As for
pod update
yeah, I've been doing
pod deintegrate/install/update
for days now 😄
👍 1
b

brandonmcansh

02/26/2024, 6:31 PM
Did you open the workspace file? And is your AS config set to use that? Google Maps will also require the cocoapod framework to be static as well
m

Mantas Latkauskas

02/27/2024, 10:42 AM
Did you open the workspace file?
- yes
And is your AS config set to use that?
- Not sure, how do I do that?
Google Maps will also require the cocoapod framework to be static as well
- 👍
If anyone's interested, I've managed to make it build after checking out Ismails KMap project, but then it would crash with some obscure error when trying to initialize the map.
Copy code
0   CoreFoundation                      0x0000000180491128 __exceptionPreprocess + 172
	1   libobjc.A.dylib                     0x000000018008412c objc_exception_throw + 56
	2   Foundation                          0x0000000180cd7c84 -[NSBundle _initUniqueWithURL:] + 0
	3   Foundation                          0x0000000180cd7cb0 +[NSBundle bundleWithURL:] + 28
	4   oddspot-app                         0x0000000102f3d398 +[GMSResourceUtils findBundleWithName:identifier:bundleForLibraryClass:productName:] + 788
	5   oddspot-app                         0x0000000102e926e0 +[GMSMapsResources sharedResources] + 236
	6   oddspot-app                         0x0000000102e9b2c8 +[GMSServices mapsBundle] + 32
	7   oddspot-app                         0x0000000102e75558 -[GMSAsyncInitServices init] + 148
	8   oddspot-app                         0x0000000102e9965c +[GMSServices preLaunchServices] + 124
	9   oddspot-app                         0x0000000102e996f0 +[GMSServices sharedServicesSync] + 112
	10  oddspot-app                         0x0000000102e82b28 -[GMSMapView initWithOptions:] + 180
	11  oddspot-app                         0x0000000102e82a58 -[GMSMapView init] + 68
	12  oddspot-app                         0x0000000103656658
Tried different things but what fixed it was removing MOKO resources lib from my app. I've already had many issues with the lib versions 0.23.0/0.24-0-alpha-X and the errors seemed related to app resources. Thank you, guys, for the help kodee happy
👍 1
j

Jhaman Das

03/06/2024, 8:17 AM
@Mantas Latkauskas I am facing smilier problem, did you get to fix this issue? Moko resources with google maps . Seems like few of images have compatibility issue.
m

Mantas Latkauskas

03/08/2024, 1:24 PM
I removed MOKO resources and used Resources from Compose Multiplatform https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-images-resources.html#qualifiers
b

Benoit Billington

04/12/2024, 8:25 PM
@Mantas Latkauskas have you got it working? I'm able to make my project build but it crashes as it no longer can find the custom fonts (using resources from kmp - moko was already removed 2 months ago)
m

Mantas Latkauskas

04/14/2024, 6:09 PM
Yeah fonts work for me fine. I store them like this
commonMain/composeResources/font/Inter-Medium.ttf
And use it like this (in commonMain) :
Copy code
@Composable
fun button() : TextStyle {
   return TextStyle(
       fontFamily = FontFamily(Font(Res.font.Inter_Medium)),
       fontSize = 24.sp,
       textAlign = TextAlign.Center,
       lineHeight = 29.sp
   )
}
👍 1
b

Benoit Billington

04/14/2024, 6:11 PM
Thanks, got it working finally. Simply forgot to uncomment the podfile line related to the composeApp module.
👌 1
103 Views