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

BollywoodVillain

11/25/2020, 12:54 PM
Kotlin Multiplatform fails on iOS while calling Ktor suspend functions. The error is
kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen
. It works fine on Android and gets the network data without issues. I created a brand new MPP project using Android Studio MPP template and added the Ktor network code from Ktor-sample/client-mpp repo. What could be the problem? The complete project is on Github at: https://github.com/samkhawase/Kotlin_MPP_Demo. I’m using ktor
1.4.2
, Kotlin
1.4
, Kotlin plugin
1.4.20-release-Studio4.1.1
, and KMM plugin
0.2.0-release-65-Studio4.1
. Here’s a snapshot of my
sourceSets
from
build.gradle.kt
(:shared)
Copy code
val ktor_version = "1.4.2"
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
                implementation("io.ktor:ktor-client-core:$ktor_version")
                implementation("io.ktor:ktor-client-serialization:$ktor_version")
            }
        }
        val commonTest by getting {
            dependencies {
                //... 
                implementation("io.ktor:ktor-client-core:$ktor_version")
            }
        }
        val androidMain by getting {
            dependencies {
                //... 
                implementation("io.ktor:ktor-client-android:$ktor_version")
                implementation("io.ktor:ktor-client-serialization-jvm:$ktor_version")
            }
        }
        val androidTest by getting {
            dependencies {
                //... 
                implementation("io.ktor:ktor-client-android:$ktor_version")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:$ktor_version")
                implementation("io.ktor:ktor-client-serialization:$ktor_version")
            }
        }
        val iosTest by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:$ktor_version")
                implementation("io.ktor:ktor-client-serialization:$ktor_version")
            }
        }
    }
Here’s the failing Swift code
Copy code
func getLocations() {
        let apiService = ApiService()
        apiService.about { (htmlString) in
            print("🦋 htmlString:\n \(htmlString)")
        }
}

struct ContentView: View {
    var body: some View {
        Text(greet()).onAppear {
            print("isMainThread: \(Thread.isMainThread)")
            getLocations()
        }
    }
}
Here’s the complete stacktrace from the iOS app.
c

Cicero

11/25/2020, 12:56 PM
Copy code
isMainThread: true
Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError.
I believe you forgot this annotation at the top of your functions
Copy code
@Throws(Exception::class)
suspend fun getMeIOS(token: String): Me? {
    return client.get<Me> {
        url("https://")
        accept(ContentType.Application.Json)
        contentType(ContentType.Application.Json)
        header(
            "Authorization",
            token
        )
    }
}
b

BollywoodVillain

11/25/2020, 12:58 PM
Interesting, In the ktor client-mpp sample it’s not there. Let me try it out
c

Cicero

11/25/2020, 12:58 PM
That’s the error that’s popping at least
b

BollywoodVillain

11/25/2020, 12:58 PM
in fact the function doesn’t even say
suspend
in the ktor-samples
c

Cicero

11/25/2020, 12:59 PM
weird
let me give you a sample of how I unwrap this on my front
b

BollywoodVillain

11/25/2020, 12:59 PM
here’s what it says in the ktor github
Copy code
package io.ktor.samples.mpp.client
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.*

internal expect val ApplicationDispatcher: CoroutineDispatcher

class ApplicationApi {
    private val client = HttpClient()

    var address = Url("<https://tools.ietf.org/rfc/rfc1866.txt>")

    fun about(callback: (String) -> Unit) {
        GlobalScope.apply {
            launch(ApplicationDispatcher) {
                val result: String = client.get {
                    url(this@ApplicationApi.address.toString())
                }
                callback(result)
            }
        }
    }
}
c

Cicero

11/25/2020, 1:00 PM
And you believe them?
🙂
b

BollywoodVillain

11/25/2020, 1:00 PM
let me give you a sample of how I unwrap this on my front
much appreciated! I was banging my head traversing the interwebz for this error
And you believe them?
Us newbies have no other choice 😅
c

Cicero

11/25/2020, 1:01 PM
let login = common.LoginModel(email: emailTextfield.text, password: passwordTextField.text, userType: String(describing:UserType.professional))           *if*(login.isNotNullOrEmpty()){       common.PostLoginKt.postLoginIOS(login: login, completionHandler: {         login, error in                   *if*(login != “”){           if let token = login {           }         }       }       }
something like this to unwrap
I believe there may be some extra }
b

BollywoodVillain

11/25/2020, 1:05 PM
However the error occur when I initialize the
ApiService
class, not when I call the suspend func. Let me check once
yes, the error is \*not\* on function call, but on the instantiation of the
ApiService
variable on Swift.
c

Cicero

11/25/2020, 1:10 PM
k
a

Arkadii Ivanov

11/25/2020, 1:10 PM
I think the code should not crash, so there is no need for @Throws annotation. The error says that it could mutate the frozen ApiService object. You can add
ensureNeverFrozen()
into its
init
section, so you will get a stack trace of the place, where it is actually gets frozen.
c

Cicero

11/25/2020, 1:10 PM
let me see what I have here
Funny, I just read your article @Arkadii Ivanov
❤️ 1
This is my sourceSet
Copy code
sourceSets {
    val ktorVersion = "1.4.2"
    val serializationVersion = "1.0.0-RC"
    val coroutineVersion = "1.3.9-native-mt-2"

    val commonMain by getting {
        val commonCore = "io.ktor:ktor-client-core:$ktorVersion"
        val commonJson = "io.ktor:ktor-client-json:$ktorVersion"
        val commonSerialization = "io.ktor:ktor-client-serialization:$ktorVersion"
        val commonSerializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion"
        val common = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"

        dependencies {
            implementation(commonCore)
            implementation(commonSerializationCore)
            implementation(commonJson)
            implementation(commonSerialization)
            implementation(common)
        }
    }
    val androidMain by getting {
        val ktorAndroid = "io.ktor:ktor-client-android:${ktorVersion}"
        dependencies {
            implementation(ktorAndroid)
        }
    }
    val iosMain by getting{
        val ktoriOS = "io.ktor:ktor-client-ios:${ktorVersion}"
        dependencies {
            implementation(ktoriOS)
        }
    }
}
Copy code
private val json = Json {
    isLenient = true; ignoreUnknownKeys = true; coerceInputValues = true; useArrayPolymorphism =
    true
}

internal val client = HttpClient() {
    install(JsonFeature) {
        serializer = KotlinxSerializer(json)
    }
}
a

Arkadii Ivanov

11/25/2020, 1:13 PM
Looks like HttpClient lambda freezes the captured ApiService object. Try to avoid the
json
field, create the JSON directly in HttpClient lambda
c

Cicero

11/25/2020, 1:14 PM
Hm, maybe you’re trying to do something different of what I did but, this is what I used to run Ktor
b

BollywoodVillain

11/25/2020, 1:15 PM
ok, so you mean this line in
ApiService.kt
is the culprit:
Copy code
url(this@ApiService.address.toString())
c

Cicero

11/25/2020, 1:15 PM
Why do you initialise your Api inside swift?
and not inside of common?
b

BollywoodVillain

11/25/2020, 1:16 PM
I am just trying to call the network service from the swift side
(I am totally new to Kotlin Multiplatform so doing things by trial and error)
c

Cicero

11/25/2020, 1:16 PM
Hm
If you try out the samples I posted it should work
b

BollywoodVillain

11/25/2020, 1:17 PM
Ok, I got several pointers let me try one by one and see which works
c

Cicero

11/25/2020, 1:17 PM
I only make a common.myFucntionCall from iOS side
a

Arkadii Ivanov

11/25/2020, 1:19 PM
Could you try this?
b

BollywoodVillain

11/25/2020, 1:20 PM
Looks like HttpClient lambda freezes the captured ApiService object. Try to avoid the 
json
 field, create the JSON directly in HttpClient lambda
That’s what I’m trying. Thanks
You can add 
ensureNeverFrozen()
 into its 
init
 section
strangely android studio can’t find
ensureNeverFrozen()
  reference when I do this:
Copy code
class ApiService {
    init {
        ensureNeverFrozen()
    }
//...
}
a

Arkadii Ivanov

11/25/2020, 1:21 PM
You need expect/actual it
b

BollywoodVillain

11/25/2020, 1:22 PM
Wow it worked! damn it feels good 😄
🎉 2
Thanks a lot guys, much appreciated!
You need expect/actual it
Do you have any pointers on how to do this?
This hands-on doc doesn’t say anything about
expect/actual
a

Arkadii Ivanov

11/25/2020, 1:25 PM
Absolutely, here is the native actual part.
Non-native variants are just no-ops
b

BollywoodVillain

11/25/2020, 1:27 PM
Ah ok, thanks will take a look. TBH this
expect/actual
is quite cool, much better than what I had to do to get C++ interop with iOS/Android.
💯 1
a

Arkadii Ivanov

11/25/2020, 1:29 PM
So when you see an
InvalidMutabilityException
, it's shows you the place where the mutation attempt of a frozen object happened. If you you add
ensureNeverFrozen
to that class, you will get another exception earlier, on freeze attempt.
n

nrobi

11/25/2020, 1:29 PM
Also if you’re using Ktor, there is
preventFreeze()
, which is essentially the same
expect/actual
and uses
ensureNeverFrozen
on the native part
❤️ 1
b

BollywoodVillain

11/25/2020, 1:30 PM
I see, I’ll add that to me notes now. Quite a different array of tools we’ve got here
c

Cicero

11/25/2020, 1:33 PM
I’m a little bit lost now about the difference in my approach and hist and the advantages of both 🤔
m

Michal Klimczak

11/30/2020, 1:15 PM
@BollywoodVillain did you eventually succeed? Because I have exactly the same issue.
Copy code
private val kotlinxSerializer = KotlinxSerializer(jsonConfig)
   
    private val httpClient: HttpClient = HttpClient {
        install(JsonFeature) {
            serializer = kotlinxSerializer
        }
    }
If I remove
serializer = kotlinxSerializer
, it doesn't crash. (Also if I add
preventFreeze
, the project doesn't compile at all with
Command PhaseScriptExecution failed with a nonzero exit code
) I have all ktor dependencies on
1.4.2
and corotuines at
1.4.2-native-mt
.
found this config here, but it crashes with the same error
Copy code
val ktorVersion = "1.4.1" //"1.4.2"
        val coroutineVersion = "1.3.9-native-mt-2" //"1.4.2-native-mt-2"
        val kotlinxSerialization = "1.0.0-RC2" //"1.0.1"
b

BollywoodVillain

12/07/2020, 12:06 PM
@Michal Klimczak A bit late to reply but I fixed the issue by moving the Json formatter inside the
KotlinXSerializer
init(). You can find the final working source code here: https://github.com/samkhawase/Kotlin_MPP_Demo
m

Michal Klimczak

12/07/2020, 12:09 PM
thanks, I'll try that. I wonder why it crashes for us but is apparently not a big deal for anyone else.
b

BollywoodVillain

12/07/2020, 12:30 PM
There’s a slight difference in your code and mine. I init the
KotlinxSerializer
inside the
install(JsonFeatue){
block. You can see my
ApiSErvice.kt
file for reference.
m

Michal Klimczak

12/07/2020, 1:20 PM
not a big deal but I find it strange - somehow this Galway app works "the right way" apparently
anyway, thank you very much for help 🙂
6 Views