Effortless use of Kotlin Multiplatform coroutines in Swift, including suspend
functions and
Flow<T>
returning members.
Kontinuity includes both a core Kotlin library, a Kotlin Symbol Processor, and various Swift Package Manager packages, depending on how you want to consume the generated code. Please make sure to use the same versions of each individual part.
In your Kotlin Multiplatform module, add the following to your build.gradle.kts file:
plugins {
id("com.google.devtools.ksp")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.mockative:kontinuity-core:2.0.1")
}
}
}
}
dependencies {
add("kspIos", "io.mockative:kontinuity-processor:2.0.1")
}
The Swift libraries are available using Swift Package Manager (SPM), by adding the following to your Package.swift file, or in your Xcode Project's Package Dependencies.
dependencies: [
.package(url: "https://github.com/mockative/Kontinuity.git", from: "<version>")
]
For each Kotlin class or interface annotated with @Kontinuity
, a wrapper class is generated,
referred to as the "Kontinuity Wrapper".
Given a Kotlin interface like the following:
package com.app.sample.tasks
@Kontinuity
interface TaskService {
val tasks: StateFlow<List<Task>>
suspend fun createTask(task: Task)
fun close()
}
A Kontinuity Wrapper will be generated:
package com.app.sample.tasks
// The Kontinuity Wrapper class takes the prefix 'K' by default
open class KTaskService(private val wrapped: TaskService) {
// Coroutine members have their names transformed with the 'K' suffix by default
val tasksK: KontinuityStateFlow<Task>
get() = wrapped.toKontinuityStateFlow()
fun refreshK(): KontinuitySuspend<Unit> =
kontinuitySuspend { wrapped.refresh() }
// Simple members don't require a name transformation
fun close() =
wrapped.close()
}
The generated Kontinuity Wrapper can be used in Swift through KontinuityCore
and KontinuityCombine
.
import KontinuityCore
import KontinuityCombine
val kotlinTaskService: TaskService
val taskService = KTaskService(wrapped: kotlinTaskService)
// Accessing the current value of a StateFlow
val tasks = getValue(of: taskService.tasksK)
// Subscribing to a Flow
val subscription = createPublisher(for: taskService.tasksK)
.sink { completion in } receiveValue: { tasks in
print("tasks: \(tasks)")
}
// Calling a suspend function
val subscription = createFuture(for: taskService.refreshK())
.sink { completion in
print("refresh: \(completion)")
} receiveValue: { unit in }
Kontinuity Wrapper classes have their names transformed from the type they're wrapping, by prefixing
the class with K
. Coroutine members of Kontinuity Wrapper classes also have their names
transformed, in order to prevent the Kotlin compiler from suffixing it with _
when compiling for
iOS/Darwin, which it does to prevent member signature clashes when interfaces are used.
See Configuration - Name Transformations for more information in how to configure Kontinuity.
By default, Kontinuity launches all coroutines in a scope using Dispatchers.Main.immediate
. You
can override this behaviour by annotating a top-level property with @KontinuityScope
. This
annotation can both be applied to the entire source set (by specifying default = true
), or to the
@Kontinuity
annotated types within a single source file:
// Specifies the coroutine scope used to launch Kontinuity coroutines of types within this source
// set(s), unless otherwise overwritten by a file-level @KontinuityScope.
@SharedImmutable
@KontinuityScope(default = true)
internal val defaultKontinuityScope = CoroutineScope(Dispatchers.Default + SuperviserJob())
// Specifies the coroutine scope used to launch Kontinuity coroutines of types within this file.
@SharedImmutable
@KontinuityScope
internal val taskServiceScope = CoroutineScope(Dispatchers.Unconfined + SuperviserJob())
interface TaskService {
// Coroutines launches within this type (and other types in this file) are launched in the
// `taskServiceScope`.
}
- Add Swift mock library and generator
- Add global
@KontinuityScope
annotation to control the default scope through a@SharedImmutable
global variable. - Add type-local
@KontinuityScope
annotation to control the default scope on a per-type basis. -
Consider rewritingSharedFlow<T>
wrapper generation to generating 2 properties, one for the flow, one for the value%MValue
.- Implementing this breaks usage of
suspend
functions returningStateFlow<T>
.
- Implementing this breaks usage of
Kontinuity is heavily inspired by rickclephas/KMP-NativeCoroutines, and is essentially a Kotlin Symbol Processor version of that Kotlin Compiler Plugin, born out of a desire to have similar features while maintaining compatibility with KSP.