KMM ios flow.combine throwing no event loop error - ios

suspend fun heyStackOverFlow(): Int {
val flow1 = flow<Int> { 1 }
val flow2 = flow<Int> { 2 }
return flow1.combine(flow2) { f1, f2 -> f1 + f2 }.single()
}
I use this in build.gradle
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2-native-mt")
...
}
}
I get this error
kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one.
I've tried playing around with actual / expected dispatchers from other questions but no success.
On android this works perfectly, on ios it doesn't.

You could check gradle dependencies as some 3rd party might be pulling in a non native-mt version of coroutines.
If that's the case you can force using the native-mt version as suggested by Philip:
version {
strictly("1.5.2-native-mt")
}

Related

Can't generate arm64 simulator with KMM

TL;DR Can't run Xcode project with KMM package on simulator on M1 (Max) MacBook.
I have tried to create a simple KMM project from the official guide where I want to import the module into my project in Xcode as a swiftpackage. This is where I found Multiplatform-swiftpackage.
I have managed to get the package over to my project, and I am able to run it on my phone, but I can't run it on the simulator as it seems to not build arm64 simulator which are very much present in the build.gradle.kts. Which leads to this error in Xcode:
ld: warning: ignoring file /Users/x/Library/Developer/Xcode/DerivedData/Urge-enlioljftsmplubupdnskjmixphi/Build/Products/Debug-iphonesimulator/UrgeNetwork.framework/UrgeNetwork, building for iOS Simulator-arm64 but attempting to link with file built for iOS Simulator-x86_64. I can't just exclude arm64 from my project as I have other packages not building for x86_64
I had friend try the same with his project and it build a combined x86_64 and arm simulator package. Then he tried 2 days later and it stopped generating it.
The simulator works when I run it from Android Studio, so it's probably the package. I can't see any other solutions to get the package to run via SPM in Xcode, so open for alternative solutions.
Any hints is greatly appreciated.
build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization") version "1.6.21"
id("com.chromaticnoise.multiplatform-swiftpackage") version "2.0.3"
}
val ktorVersion = "2.0.2"
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "UrgeNetwork"
}
}
multiplatformSwiftPackage {
swiftToolsVersion("5.5")
targetPlatforms {
iOS { v("15") }
}
outputDirectory(File(rootDir, "/"))
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
}
val androidTest by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
}
}
android {
compileSdk = 32
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 29
targetSdk = 32
}
}
kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java) {
binaries.all {
binaryOptions["memoryModel"] = "experimental"
}
}

Failing to generate libraryname.xcframework in kotlin multiplatform mobile (KMM)

I was trying to generate the XCFramework with Kotlin 1.5.31 containing the targets for iOSArm64 and iOSX64 .
With the below build.gradle.kt , it generates a FatFrameworks . I’m failing to generate XCFrameworks .
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
kotlin {
val xcFramework = XCFramework(libName)
android()
ios {
binaries.framework(libName) {
xcFramework.add(this)
}
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val androidMain by getting {
dependencies {
implementation("com.google.android.material:material:1.2.1")
}
}
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13")
}
}
val iosMain by getting
val iosTest by getting
}
}
And also I have included the tasks in build.gradle.kts :
tasks {
register(“buildDebugXCFramework”)
register(“buildReleaseXCFramework”)
register(“publishDevFramework”)
register(“publishFramework”)
}
This is the output I got : fatframeowrks generated but not the libraryname.xcframeworks
If Any suggestions to generate XCFrameworks with targets iOSArm64 and iOSX64 ? , it would be helpful , Thank you .
I think that following the documentation might help here.
Please set library name by the baseName option and build the final XCFramework by running assembleXCFramework Gradle task.
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
kotlin {
val xcFramework = XCFramework()
android()
ios {
binaries.framework() {
baseName = "libName"
xcFramework.add(this)
}
}
...

Kotlin Multiplatform Library: Unable to generate .framework for iOS

I am new to Android/KotlinMultiplatform , I am trying to create a library for iOS/Android using Kotlin Multiplatform.
When I run the command on terminal
./gradlew :shared:packForXcode
It succeeds but could not find a /build/xcode-frameworks folder inside the root folder.
Could anyone help me to find where it is going wrong...?
IntelliJ CE Version : 2020.2.3
My Gradle file Content:
plugins {
id("org.jetbrains.kotlin.multiplatform") version "1.4.10"
id("com.android.library")
id("kotlin-android-extensions")
"maven-publish"
}
repositories {
mavenCentral()
}
group "me.myname"
version "0.0.1"
kotlin {
targets {
android()
ios {
binaries {
framework {
baseName = "MyLib"
}
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
}
}
val androidMain by getting {
dependencies { }
}
val iosMain by getting {
dependencies { }
}
}
}
android {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
}
val packForXcode by tasks.creating(Sync::class) {
val targetDir = File(buildDir, "xcode-frameworks")
/// selecting the right configuration for the iOS
/// framework depending on the environment
/// variables set by Xcode build
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
val framework = kotlin.targets
.getByName<KotlinNativeTarget>(
if(isiOSDevice) {
"iosArm64"
} else {
"iosX64"
}
)
.binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
from({ framework.outputDirectory })
into(targetDir)
println("Build Folder => $targetDir")
/// generate a helpful ./gradlew wrapper with embedded Java path
doLast {
val gradlew = File(targetDir, "gradlew")
gradlew.writeText("#!/bin/bash\n"
+ "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
+ "cd '${rootProject.rootDir}'\n"
+ "./gradlew \$#\n")
gradlew.setExecutable(true)
}
}
tasks.build.dependsOn("packForXCode")
UPDATE
Project Created using IntelliJ IDEA, as below screenshot:
My project structure looks like below:
I've only been able to see the template of your screenshot by using
IntelliJ 2020.2.3 Ultimate
This template doesn't have the packForXcode task set by default, so you would have put it by hands I suppose.
Anyway, with a cleaned project, if you run it, you could have the debug framework in the build folder where you want to have it.
You should have, of course, at least one source (Greeting.kt) file like the one I've shown you in my pic.
I suggest you to look deep at the documentation starting from here and here.
If I remember correctly, this task is not designed to be executed manually. It should be triggered as a part of the Xcode project build, see in the documentation. Please try to follow the steps from the documentation, and see if the framework connects and works fine from Xcode.

withTimeout function gives IllegalStateException: There is no event loop. Use runBlocking { ... } to start one. in Kotlin Multiplatform iOS client

Update:
It works if I first execute a coroutine without timeout and then withTimeout. But If I execute a coroutine withTimeout first then it gives me an error. same goes for Async as well.
I am creating a demo kotlin multiplatform application where I am executing an API call with ktor.
I want to have a configurable timeout function on ktor request so I am using withTimeout at coroutine level.
Here is my function call with network API.
suspend fun <T> onNetworkWithTimeOut(
url: String,
timeoutInMillis: Long,
block: suspend CoroutineScope.() -> Any): T {
return withTimeout(timeoutInMillis) {
withContext(dispatchers.io, block)
} as T
}
suspend fun <T> onNetworkWithoutTimeOut(url: String, block: suspend CoroutineScope.() -> Any): T {
return withContext(dispatchers.io, block) as T
}
Here is my AppDispatcher class for the iOSMain module.
#InternalCoroutinesApi
actual class AppDispatchersImpl : AppDispatchers {
#SharedImmutable
override val main: CoroutineDispatcher =
NsQueueDispatcher(dispatch_get_main_queue())
#SharedImmutable
override val io: CoroutineDispatcher =
NsQueueDispatcher(dispatch_get_main_queue())
internal class NsQueueDispatcher(
#SharedImmutable private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
NSRunLoop.mainRunLoop().performBlock {
block.run()
}
}
}
}
so the function with the timeout gives me the following error in iOS client.
kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one.
I am using 1.3.2-native-mt-1 version of the kotlin-coroutine-native.
I have created a sample demo application at the following URL.
https://github.com/dudhatparesh/kotlin-multiplat-platform-example
So, as mentioned in comment above I had similar issue but turned out that it wasn't picking up the native-mt version due to transitive dependencies in other libraries. Added following and it's resolving now.
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-core-native')
{
version {
strictly '1.3.3-native-mt'
}
}
Also note guidance in https://github.com/Kotlin/kotlinx.coroutines/blob/native-mt/kotlin-native-sharing.md
Starting to make use of this in https://github.com/joreilly/PeopleInSpace
If you want to use [withTimeout] functions in coroutines you have to modify your Dispatcher to implement Delay interface. Here is an example of how this can be achieved:
#UseExperimental(InternalCoroutinesApi::class)
class UI : CoroutineDispatcher(), Delay {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) {
try {
block.run()
} catch (err: Throwable) {
throw err
}
}
}
#InternalCoroutinesApi
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
try {
with(continuation) {
resumeUndispatched(Unit)
}
} catch (err: Throwable) {
throw err
}
}
}
#InternalCoroutinesApi
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
val handle = object : DisposableHandle {
var disposed = false
private set
override fun dispose() {
disposed = true
}
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
try {
if (!handle.disposed) {
block.run()
}
} catch (err: Throwable) {
throw err
}
}
return handle
}
}
This solution can be easily modified for your needs.
More information can be found in this thread.
UPDATE
At the moment there is a version 1.3.9-native-mt of kotlinx:kotlinx-coroutines-core artifact which gives the ability to use Dispatchers.Main on ios platform (it supports delay out of the box). It even supports Dispatchers.Default which is used for background work. You can read docs in native-mt branch. Worth noting that the version for ios should be set strictly:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9-native-mt") {
version {
strictly('1.3.9-native-mt')
}
}
Sometimes ios app has a different async requirement with an android app.
Use this code for temporary dispatch problem
object MainLoopDispatcher: CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
NSRunLoop.mainRunLoop().performBlock {
block.run()
}
}
}
Please see the forum for this issue: https://github.com/Kotlin/kotlinx.coroutines/issues/470

Kotlin multiplatform lambda call without Unit return type

I have a multiplatform Kotlin project for Android and iOS. The functions in common module are written like this:
fun test(successCallback: (String) -> Unit, errorCallback: (Error) -> Unit) {
successCallback("success")
}
Android usage of function:
Common.test(successCallback = {
Log.d(TAG, it)
},
errorCallback = {
Log.d(TAG, it)
}
})
iOS usage of function:
Common.test(successCallback: { it in
print(it)
return KotlinUnit()
}, errorCallback: { error in
print(error)
return KotlinUnit()
})
JVM world seems to handle the return type (Unit) by itself (nothing needs to be returned), while you must return KotlinUnit() in iOS.
I would like to achieve the same on iOS (callback without return KotlinUnit())

Resources