Search code examples
androidiosktorkotlin-multiplatform

ktor dependencies not resolved in ios module of multiplatform project


I have a kotlin-multiplatform project targeting iOS and Android. Ktor http client is used in common module.

Everything works great with Android app. But when building project with iOS lib, I receive following exceptions:

> Task :app:compileKotlinIos FAILED
src/commonMain/kotlin/com/ottamotta/mozoli/api/MozoliApiKtor.kt:4:8: error: unresolved reference: io
import io.ktor.client.HttpClient
       ^
src/commonMain/kotlin/com/ottamotta/mozoli/api/MozoliApiKtor.kt:5:8: error: unresolved reference: io
import io.ktor.client.features.feature

...and other ones, saying none of the ktor dependencies have been resolved.

Here is build.gradle:

plugins {
    id 'kotlin-multiplatform' version '1.3.10'
}
repositories {
    google()
    jcenter()
    mavenCentral()
    maven { url "https://kotlin.bintray.com/kotlinx" }
}

ext {
    support_lib_version = '28.0.0'
    ktor_version = '1.0.0'
}

def keystorePropertiesFile = rootProject.file("./app/secret.properties");
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    buildToolsVersion '28.0.3'
    defaultConfig {
        applicationId "com.ottamotta.mozoli"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "https"]
    }
    buildTypes {

        debug {
            resValue "string", "com_auth0_client_id", keystoreProperties['com_auth0_client_id']
        }
        release {
            resValue "string", "com_auth0_client_id", keystoreProperties['com_auth0_client_id']
            minifyEnabled false
        }
    }

    lintOptions {
        abortOnError false
    }


}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "com.android.support:recyclerview-v7:${support_lib_version}"
    implementation "com.android.support:appcompat-v7:${support_lib_version}"
    implementation 'com.squareup.picasso:picasso:2.71828'

    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7"

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1")
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'

    implementation "com.auth0.android:auth0:1.14.1"

    androidTestImplementation 'com.android.support.test:runner:1.0.2'
}

kotlin {
    targets {
        fromPreset(presets.android, 'android')
        // This preset is for iPhone emulator
        // Switch here to presets.iosArm64 (or iosArm32) to build library for iPhone device
        fromPreset(presets.iosX64, 'ios') {
            compilations.main.outputKinds('FRAMEWORK')
        }
    }
    sourceSets {
        commonMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
                implementation "io.ktor:ktor-client:$ktor_version"
                implementation "io.ktor:ktor-client-json:$ktor_version"
                implementation "io.ktor:ktor-client-jackson:$ktor_version"
            }
        }
        commonTest {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-test-common'
                implementation 'org.jetbrains.kotlin:kotlin-test-annotations-common'
            }
        }
        androidMain {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-stdlib'
                implementation "io.ktor:ktor-client-android:$ktor_version"
            }
        }
        androidTest {
            dependencies {
                implementation 'org.jetbrains.kotlin:kotlin-test'
                implementation 'org.jetbrains.kotlin:kotlin-test-junit'
            }
        }
        iosMain {
            dependencies {
                implementation("io.ktor:ktor-client-ios:$ktor_version")
            }
        }
        iosTest {
        }
    }
}

task copyFramework {
    def buildType = project.findProperty("kotlin.build.type") ?: "DEBUG"
    def target = project.findProperty("kotlin.target") ?: "ios"
    dependsOn "link${buildType.toLowerCase().capitalize()}Framework${target.capitalize()}"

    doLast {
        def srcFile = kotlin.targets."$target".compilations.main.getBinary("FRAMEWORK", buildType)
        def targetDir = getProperty("configuration.build.dir")
        copy {
            from srcFile.parent
            into targetDir
            include 'app.framework/**'
            include 'app.framework.dSYM'
        }
    }
}

Here is the code of the file from common module which generates errors:

package com.ottamotta.mozoli.api

import com.ottamotta.mozoli.*
import io.ktor.client.HttpClient
import io.ktor.client.features.feature
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.JsonSerializer
import io.ktor.client.features.json.defaultSerializer
import io.ktor.client.request.header
import io.ktor.client.request.request
import io.ktor.client.request.url
import io.ktor.http.HttpMethod


class MozoliApiKtor(
    private val serverUrl: String,
    private var jsonSerializer: JsonSerializer? = null,
    private val tokenProvider: suspend () -> String?
) : MozoliApi {

    private val client: HttpClient

    private val AUTH_HEADER = "Authorization";
    private val TOKEN_PREFIX = "Bearer "

    init {
        client = HttpClient {
            install(JsonFeature) {
                serializer = jsonSerializer ?: defaultSerializer()
            }
        }
        jsonSerializer = client.feature(JsonFeature)?.serializer
    }

    override suspend fun getUserProfile(): User {
        return client.request {
            url("${serverUrl}/user/")
            method = HttpMethod.Get
            header(AUTH_HEADER, TOKEN_PREFIX + tokenProvider())
        }
    }

    override suspend infix fun getEventsByCity(cityId: String): List<Event> {
        return client.request {
                url("${serverUrl}/event/city/${cityId}")
                method = HttpMethod.Get
                header(AUTH_HEADER, TOKEN_PREFIX + tokenProvider())
            }

    }

}

Solution

  • I was having the same exact issue as you. Built and works flawlessly for the Android app, but the iOS module cannot find any Ktor, coroutine, or kotlinx serialization class.

    Following along https://github.com/adrianbukros/github-multiplatform-example, and trying to copy their set up, I finally got the packForXCode task to work by copying some of their gradle set up namely:

    Their common module build.gradle, which should look like this:

    apply plugin: 'kotlin-multiplatform'
    apply plugin: 'kotlinx-serialization'
    
    kotlin {
        targets {
            fromPreset(presets.jvm, "android")
    
            fromPreset(presets.iosX64, "ios_x86_64")
            fromPreset(presets.iosArm64, "ios_arm64")
            configure([ios_x86_64, ios_arm64]) {
                compilations.main.outputKinds("FRAMEWORK")
            }
        }
    
        sourceSets {
            commonMain.dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib"
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
    
                implementation "io.ktor:ktor-client-core:$ktor_version"
                implementation "io.ktor:ktor-client-json:$ktor_version"
            }
    
            androidMain.dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
    
                implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
                implementation "io.ktor:ktor-client-json-jvm:$ktor_version"
            }
    
            iosMain.dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version"
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
    
                implementation "io.ktor:ktor-client-ios:$ktor_version"
                implementation "io.ktor:ktor-client-core-ios:$ktor_version"
                implementation "io.ktor:ktor-client-json-ios:$ktor_version"
            }
            configure([ios_x86_64Main, ios_arm64Main]) {
                dependsOn iosMain
            }
        }
    }
    
    // workaround for https://youtrack.jetbrains.com/issue/KT-27170
    configurations {
        compileClasspath
    }
    
    task packForXCode(type: Sync) {
        final File frameworkDir = new File(buildDir, "xcode-frameworks")
    
        final String configuration = project.findProperty("CONFIGURATION")?.toUpperCase() ?: "DEBUG"
        final String arch = project.findProperty("ARCHS") ?: "x86_64"
    
        dependsOn kotlin.targets."ios_${arch}".compilations.main.linkTaskName("FRAMEWORK", configuration)
    
        from { kotlin.targets."ios_${arch}".compilations.main.getBinary("FRAMEWORK", configuration).parentFile }
        into frameworkDir
    }
    
    tasks.build.dependsOn packForXCode
    

    And their settings.gradle file which should look like this:

    enableFeaturePreview("GRADLE_METADATA") // IMPORTANT!
    
    include 'commoncode'
    include 'app'
    

    If it's still not working, look at all of their *.gradle and gradle.* files and see how yours are different.

    That finally allowed the task to pass for me, and I can use my Kotlin Ktor code in XCode. However, now Android Studio is saying "Kotlin is not configured" for the shared iOS module. I will report back if I figure out why, and if you find something, please share!

    EDIT Changed the common module build.gradle to look like this, seems like everything is working well!

    apply plugin: 'kotlin-multiplatform'
    apply plugin: 'kotlinx-serialization'
    
    kotlin {
        targets {
            fromPreset(presets.jvm, 'android')
    
            // Change to `presets.iosArm64` to deploy the app to iPhone
            fromPreset(presets.iosX64, 'ios') {
                compilations.main.outputKinds('FRAMEWORK')
            }
        }
    
        sourceSets {
            commonMain.dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib"
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version"
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
    
                implementation "io.ktor:ktor-client-core:$ktor_version"
                implementation "io.ktor:ktor-client-json:$ktor_version"
            }
    
            androidMain.dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
    
                implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
                implementation "io.ktor:ktor-client-json-jvm:$ktor_version"
            }
    
            iosMain.dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version"
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
    
                implementation "io.ktor:ktor-client-ios:$ktor_version"
                implementation "io.ktor:ktor-client-core-ios:$ktor_version"
                implementation "io.ktor:ktor-client-json-ios:$ktor_version"
            }
        }
    }
    
    // workaround for https://youtrack.jetbrains.com/issue/KT-27170
    configurations {
        compileClasspath
    }
    
    task packForXCode(type: Sync) {
        final File frameworkDir = new File(buildDir, "xcode-frameworks")
        final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
    
        inputs.property "mode", mode
        dependsOn kotlin.targets.ios.compilations.main.linkTaskName("FRAMEWORK", mode)
    
        from { kotlin.targets.ios.compilations.main.getBinary("FRAMEWORK", mode).parentFile }
        into frameworkDir
    
        doLast {
            new File(frameworkDir, 'gradlew').with {
                text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
                setExecutable(true)
            }
        }
    }
    
    tasks.build.dependsOn packForXCode