SlideShare a Scribd company logo
Coroutines for Kotlin
Multiplatform in Practise
Christian Melchior | Lead Engineer | MongoDB Realm | @chrmelchior
Realm
● Open Source Object Database
● C++ with Language SDK’s on top
● First Android release in 2014
● Part of MongoDB since 2019
● Currently building a Kotlin Multiplatform SDK at
https://github.com/realm/realm-kotlin/
By MongoDB
Coroutines is a big topic
Shared
iOSApp AndroidApp JSApp
Coroutines is a big topic
Shared
iOSApp AndroidApp JSApp
Coroutines is a big topic
Shared
iOSApp AndroidApp JSApp
Coroutines is a big topic
Shared
iOSApp AndroidApp JSApp
Kotlin Common Constraints
Dispatchers
Memory model
Testing
Consuming Coroutines in Swift
Completion Handlers
Combine
Async/Await
Coroutines in Shared Code
Adding Coroutines - native-mt or not
kotlin {
sourceSets {
commonMain {
dependencies {
// Choose one
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2-native-mt")
}
}
}
https://kotlinlang.org/docs/releases.html#release-details
https://github.com/Kotlin/kotlinx.coroutines/issues/462
native-mt
standard
Less bugs
Only one thread on Kotlin Native
Current standard
Ktor ships with this
Multithread support on Kotlin Native
Will become the standard
native-mt
standard
Less bugs
Only one thread on Kotlin Native
Current standard
Ktor ships with this
Multithread support on Kotlin Native
Will become the standard
Dispatchers
val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main)
viewScope.launch {
val dbObject = withContext(Dispatchers.IO) {
val networkObject = runNetworkRequest()
writeToDB(networkObject)
}
val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) }
updateUI(uiObject)
}
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Unconfined
Reuse parent thread
Default
JVM:Limited by CPUs
available.
Native: Single thread
IO
JVM Only Main
Beware of Dragons
Dispatchers
Custom
CoroutineDispatcher
Integrate with
framework run loops
Dispatchers.Main
val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main)
viewScope.launch {
val dbObject = withContext(Dispatchers.IO) {
val networkObject = runNetworkRequest()
writeToDB(networkObject)
}
val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) }
updateUI(uiObject)
}
Dispatchers.Main
val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main)
viewScope.launch {
val dbObject = withContext(Dispatchers.IO) {
val networkObject = runNetworkRequest()
writeToDB(networkObject)
}
val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) }
updateUI(uiObject)
}
iOS -> Deadlock
JVM -> Module with the Main dispatcher is missing. Add dependency providing the Main
dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as
'kotlinx-coroutines-core'
kotlin-coroutines-test
val customMain = singleThreadDispatcher("CustomMainThread")
Dispatchers.setMain(customMain)
runBlockingTest {
delay(100)
doWork()
}
https://github.com/Kotlin/kotlinx.coroutines/issues/1996
Inject Dispatchers
expect object Platform {
val MainDispatcher: CoroutineDispatcher
val DefaultDispatcher: CoroutineDispatcher
val UnconfinedDispatcher: CoroutineDispatcher
val IODispatcher: CoroutineDispatcher
}
actual object Platform {
actual val MainDispatcher
get() = singleThreadDispatcher("CustomMainThread")
actual val DefaultDispatcher
get() = Dispatchers.Default
actual val UnconfinedDispatcher
get() = Dispatchers.Unconfined
actual val IODispatcher
get() = singleThreadDispatcher("CustomIOThread")
}
Inject Dispatchers
val viewScope = CoroutineScope(CoroutineName("MyScope") + Platform.MainDispatcher)
viewScope.launch {
val dbObject = withContext(Platform.IODispatcher) {
val networkObject = runNetworkRequest()
writeToDB(networkObject)
}
val uiObject = withContext(Platform.DefaultDispatcher) {
UIObject.from(dbObject)
}
updateUI(uiObject)
}
Advice #1: Always inject
Dispatchers
Kotlin Native Memory Model
class MyObject {
var name: String = "Jane Doe"
var age: Int = 42
}
Suspend fun automaticFreeze() {
val obj = MyObject()
withContext(Dispatchers.Default) {
obj.name = "John Doe"
}
}
Kotlin Native Memory Model
class MyObject {
var name: String = "Jane Doe"
var age: Int = 42
}
Suspend fun automaticFreeze() {
val obj = MyObject()
withContext(Dispatchers.Default) {
obj.name = "John Doe"
}
}
kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen
io.realm.kotlin.practicalcoroutines.AllTests.MyObject@41a0a068
Kotlin Native Memory Model
class SafeMyObject {
val name: AtomicRef<String> = atomic("Jane Doe")
val age: AtomicInt = atomic(42)
}
fun nativeMemoryModel_safeAccess() {
val obj = SafeMyObject()
runBlocking(Dispatchers.Default) {
obj.name.value = "Foo"
}
}
https://github.com/Kotlin/kotlinx.atomicfu
Freeze in constructors
class SafeKotlinObj {
val name: AtomicRef<String> = atomic("Jane Doe")
val age: AtomicInt = atomic(42)
init {
this.freeze()
}
}
Advice #2: Code and
test against the most
restrictive memory
model
Kotlin Common != Kotlin KMM
public expect fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
public expect fun threadId(): String
public expect fun singleThreadDispatcher(id: String): CoroutineDispatcher
public expect fun <T> T.freeze(): T
public expect val <T> T.isFrozen: Boolean
public expect fun Any.ensureNeverFrozen()
Kotlin Common != Kotlin KMM
public expect fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
public expect fun threadId(): String
public expect fun singleThreadDispatcher(id: String): CoroutineDispatcher
public expect fun <T> T.freeze(): T
public expect val <T> T.isFrozen: Boolean
public expect fun Any.ensureNeverFrozen()
Testing, RunBlocking and Deadlocks
val dispatcher = singleThreadDispatcher("CustomThread")
val otherDispatcher = singleThreadDispatcher("OtherThread")
runBlocking(dispatcher) {
runBlocking(otherDispatcher) {
doWork()
}
}
Testing, RunBlocking and Deadlocks
val dispatcher = singleThreadDispatcher("CustomThread")
runBlocking(dispatcher) {
runBlocking(dispatcher) {
doWork()
}
}
Works on iOS
Deadlocks on JVM
RunBlocking - Debugging
Advice #3: Test with all
dispatchers running on
the same thread
Advice #4: Test on both
Native and JVM
Summary
● Use native-mt.
● Always Inject Dispatchers.
● Avoid Dispatchers.Main in tests.
● Assume that all user defined Dispatchers are
running on the same thread.
● KMM is not Kotlin Common.
● The frozen memory model also works on JVM.
Write your shared code as for Kotlin Native.
Shared Code
Coroutines and Swift
Completion Handlers
// Kotlin
suspend fun doWork(): KotlinObj {
println("Being called on: ${threadId()}")
return KotlinObj()
}
// Swift
print("Starting work on: (PlatformUtilsKt.threadId())")
let model = KotlinModel()
model.doWork() { (data: KotlinObj?, error: Error?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!.message)
}
Completion Handlers
// Kotlin
suspend fun doWork(): KotlinObj {
println("Being called on: ${threadId()}")
return KotlinObj()
}
// Swift
print("Starting work on: (PlatformUtilsKt.threadId())")
let model = KotlinModel()
model.doWork() { (data: KotlinObj?, error: Error?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!.message)
}
Starting work from: 2183729
Being called on: 2183729
Result received on: 2183729
Hello from Kotlin
Completion Handlers and Threads
let model = KotlinModel()
DispatchQueue.global(qos: .userInitiated).async {
model.doWork() { data, error in
print("Result received on: (PlatformUtilsKt.threadId())")
}
}
Completion Handlers and Threads
let model = KotlinModel()
DispatchQueue.global(qos: .userInitiated).async {
model.doWork() { data, error in
print("Result received on: (PlatformUtilsKt.threadId())")
}
}
Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to
access non-shared io.realm.kotlin.practicalcoroutines.PracticalCoroutines@814208 from
other thread
Advice #5: All public
API’s should be frozen
Completion Handlers and Threads
let model = PlatformUtilsKt.freezeObject(obj: KotlinModel()) as! KotlinModel
DispatchQueue.global(qos: .userInitiated).async {
model.doWork() { data, error in
print("Result received on: (PlatformUtilsKt.threadId())")
}
}
Completion Handlers and Threads
let model = PlatformUtilsKt.freezeObject(obj: KotlinModel()) as! KotlinModel
DispatchQueue.global(qos: .userInitiated).async {
model.doWork() { data, error in
print("Result received on: (PlatformUtilsKt.threadId())")
}
}
2021-10-13 09:47:52.451417+0200 iosApp[83765:2103465] *** Terminating app due to
uncaught exception 'NSGenericException', reason: 'Calling Kotlin suspend functions from
Swift/Objective-C is currently supported only on main thread'
Completion Handlers and Threads
suspend fun doWorkInBackground(): KotlinObj {
return withContext(Dispatchers.Default) {
println("Being called on: ${threadId()}")
KotlinObj()
}
}
let model = KotlinModel()
print("Starting on: (PlatformUtilsKt.threadId())")
model.doWorkInBackground() { (data: KotlinObj?, error: Error?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!.message)
}
Completion Handlers and Threads
suspend fun doWorkInBackground(): KotlinObj {
return withContext(Dispatchers.Default) {
println("Being called on: ${threadId()}")
KotlinObj()
}
}
let model = KotlinModel()
print("Starting on: (PlatformUtilsKt.threadId())")
model.doWorkInBackground() { (data: KotlinObj?, error: Error?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!.message)
}
Starting on: 29667
Being called on: 30896
Result received on: 29667
Hello from Kotlin
Advice #6: Control
Context in shared code
Completion Handlers and error reporting
suspend fun doWorkThatThrows(): KotlinObj {
throw RuntimeException("Error from Kotlin")
}
Completion Handlers and error reporting
suspend fun doWorkThatThrows(): KotlinObj {
throw RuntimeException("Error from Kotlin")
}
Exception doesn't match @Throws-specified class list and thus isn't propagated from
Kotlin to Objective-C/Swift as NSError.
It is considered unexpected and unhandled instead. Program will be terminated.
Uncaught Kotlin exception: kotlin.RuntimeException: Error from Kotlin
Completion Handlers and error reporting
@Throws(RuntimeException::class)
suspend fun doWorkThatThrows(): KotlinObj {
throw RuntimeException("Error from Kotlin")
}
model.doWorkThatThrows { (data: KotlinObj?, error: Error?) in
if (error != nil) {
handleError(error!)
} else {
handleResult(data!)
}
}
Flows
fun listenToFlow(): Flow<String> = flowOf("Hello", "from", "Kotlin")
class Collector<T>: Kotlinx_coroutines_coreFlowCollector {
let callback:(T) -> Void
init(callback: @escaping (T) -> Void) {
self.callback = callback
}
func emit(value: Any?, completionHandler: @escaping (KotlinUnit?, Error?) -> Void) {
callback(value as! T)
completionHandler(KotlinUnit(), nil)
}
}
model.listenToFlow().collect(collector: Collector<String> { (data: String) in
print(data)
}) { (unit, error) in
print("Done")
}
https://stackoverflow.com/questions/64175099/listen-to-kotlin-coroutine-flow-from-ios
https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org
/jetbrains/kotlinconf/FlowUtils.kt
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
internal fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
fun listenToFlow (): CommonFlow<String> = flowOf("Hello", "from",
"Kotlin").asCommonFlow().freeze()
Flows
Flows
// Subscribe to flow
let job: Closeable = model.listenToFlow().watch { (data: NSString?) in
print(data!)
}
// Unsubscribe
job.close()
Flows
// Subscribe to flow
let job: Closeable = model.listenToFlow().watch { (data: NSString?) in
print(data!)
}
Starting on: 2370807
Receiving on: 2370807
Hello
from
Kotlin
Flows - Threading controlled from Kotlin
print("Main thread: (PlatformUtilsKt.threadId())")
DispatchQueue.global(qos: .userInitiated).async {
print("This is run on a background queue: (PlatformUtilsKt.threadId())")
KotlinModel().listenToFlow().watch { (data: NSString?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!)
}
}
Flows - Threading controlled from Kotlin
print("Main thread: (PlatformUtilsKt.threadId())")
DispatchQueue.global(qos: .userInitiated).async {
print("This is run on a background queue: (PlatformUtilsKt.threadId())")
KotlinModel().listenToFlow().watch { (data: NSString?) in
print("Result received on: (PlatformUtilsKt.threadId())")
print(data!)
}
}
Main thread: 2358850
This is run on a background queue: 2359090
Results received on: 2358850
Hello
from
Kotlin
Flows - Threading controlled from Kotlin
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
} Main thread: 2358850
This is run on a background queue: 2359090
Results received on: 2358850
Hello
from
Kotlin
Flows - Error handling
fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin")
.onEach {
throw RuntimeException("Crash in Kotlin Flow")
}
.asCommonFlow()
let model = KotlinModel()
model.listToFlowThatThrows().watch { data in
print(data!)
}
Flows - Error handling
fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin")
.onEach {
throw RuntimeException("Crash in Kotlin Flow")
}
.asCommonFlow()
let model = KotlinModel()
model.listToFlowThatThrows().watch { data in
print(data!)
}
Uncaught Kotlin exception: kotlin.Throwable: The process was terminated due to the
unhandled exception thrown in the coroutine [StandaloneCoroutine{Cancelling}@2b852c8,
MainDispatcher]: Crash in Kotlin Flow
Flows - Error handling
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T?, Exception?) -> Unit): Closeable {
val job = Job()
onEach {
block(it, null)
}
.catch { error: Throwable ->
// Only pass on Exceptions.
// This also correctly converts Exception to Swift Error.
if (error is Exception) {
block(null, error)
}
throw error // Then propagate exception on Kotlin side
}
.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
Flows - Error handling
worker.listToFlowThatThrows().watch { (data: NSString?, error: KotlinException?) in
if (error != nil) {
print(error!.message!)
} else {
print(data!)
}
}
Convert Flows to Combine
public struct KotlinFlowPublisher<T: AnyObject>: Publisher {
public typealias Output = T
public typealias Failure = Never
private let flow: CommonFlow<T>
public init(flow: CommonFlow<T>) {
self.flow = flow
}
public func receive<S: Subscriber>(subscriber: S) where S.Input == T, S.Failure == Failure {
let subscription = KotlinFlowSubscription(flow: flow, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/
Convert Flows to Combine
final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure {
private var subscriber: S?
private var job: Kotlinx_coroutines_coreJob? = nil
private let flow: CommonFlow<T>
init(flow: CommonFlow<T>, subscriber: S) {
self.flow = flow
self.subscriber = subscriber
job = flow.subscribe(
onEach: { el in subscriber.receive(el!) },
onComplete: { subscriber.receive(completion: .finished) },
onThrow: { error in debugPrint(error) }
)
}
func cancel() {
subscriber = nil
job?.cancel(cause: nil)
}
func request(_ demand: Subscribers.Demand) {}
}
}
Convert Flows to Combine
final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure {
private var subscriber: S?
private var job: Kotlinx_coroutines_coreJob? = nil
private let flow: CommonFlow<T>
init(flow: CommonFlow<T>, subscriber: S) {
self.flow = flow
self.subscriber = subscriber
job = flow.subscribe(
onEach: { el in subscriber.receive(el!) },
onComplete: { subscriber.receive(completion: .finished) },
onThrow: { error in debugPrint(error) }
)
}
func cancel() {
subscriber = nil
job?.cancel(cause: nil)
}
func request(_ demand: Subscribers.Demand) {}
}
}
Convert Flows to Combine
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
// ...
// Expose Flow in a way that makes it possible to convert to Publisher in Swift.
fun subscribe(
onEach: (item: T) -> Unit,
onComplete: () -> Unit,
onThrow: (error: Throwable) -> Unit
) = this
.onEach { onEach(it) }
.catch { onThrow(it) }
.onCompletion { onComplete() }
.launchIn(CoroutineScope(Dispatchers.Main + job))
}
Convert Flows to Combine - Usage
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Convert Flows to Combine - Usage
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Mainthread: 3392369
Sending 'Hello' on: 3392369
Sending 'from' on: 3392369
Sending 'Kotlin' on: 3392369
Receiving 'Hello' on: 3392369
Receiving 'from' on: 3392369
Receiving 'Kotlin' on: 3392369
Controlling threads with Combine
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.subscribe(on: DispatchQueue.global())
.map { (data: NSString) -> String in
print("""
Map (data) on: 
(PlatformUtilsKt.threadId())
""")
return data as String
}
.receive(on: DispatchQueue.main)
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Controlling threads with Combine
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.subscribe(on: DispatchQueue.global())
.map { (data: NSString) -> String in
print("""
Map (data) on: 
(PlatformUtilsKt.threadId())
""")
return data as String
}
.receive(on: DispatchQueue.main)
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Mainthread: 3392369
Sending 'Hello' on: 3392369
Map 'Hello' on: 3392369
Sending 'from' on: 3392369
Map 'from' on: 3392369
Sending 'Kotlin' on: 3392369
Map 'Kotlin' on: 3392369
Receiving 'Hello' on: 3392369
Receiving 'from' on: 3392369
Receiving 'Kotlin' on: 3392369
Controlling threads with Combine
let serialBackgroundQueue = DispatchQueue.init(label: "background")
KotlinFlowPublisher<NSString>(flow: worker.listenToFlow())
.subscribe(on: DispatchQueue.global())
.receive(on: serialBackgroundQueue)
.map { (data: NSString) -> String in
print("""
Map (data) on: 
(PlatformUtilsKt.threadId())
l """)
return data as String
}
.receive(on: DispatchQueue.main)
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Controlling threads with Combine
let serialBackgroundQueue = DispatchQueue.init(label: "background")
KotlinFlowPublisher<NSString>(flow: worker.listenToFlow())
.subscribe(on: DispatchQueue.global())
.receive(on: serialBackgroundQueue)
.map { (data: NSString) -> String in
print("""
Map (data) on: 
(PlatformUtilsKt.threadId())
""")
return data as String
}
.receive(on: DispatchQueue.main)
.sink { data in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
}
.store(in: &self.jobs)
Main thread: 3420316
Sending 'Hello' on: 3420316
Sending 'from' on: 3420316
Sending 'Kotlin' on: 3420316
Map Hello on: 3420430
Map from on: 3420430
Map Kotlin on: 3420430
Receiving 'Hello' on: 3420316
Receiving 'from' on: 3420316
Receiving 'Kotlin' on: 3420316
Combine with error handling - Swift
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.tryMap { _ in throw DummyError() }
.sink(receiveCompletion: { (error) in
print("(String(describing: error))")
}, receiveValue: { (data) in
print(data)
})
.store(in: &self.jobs)
Combine with error handling - Swift
KotlinFlowPublisher<NSString>(flow: model.listenToFlow())
.tryMap { _ in throw DummyError() }
.sink(receiveCompletion: { (error) in
print("(String(describing: error))")
}, receiveValue: { (data) in
print(data)
})
.store(in: &self.jobs)
Main thread: 3477630
Sending 'Hello' on: 3477630
Canceling publisher
failure(iosApp.DummyError())
Sending 'from' on: 3477630
Sending 'Kotlin' on: 3477630
Flow complete
Combine with error handling - Swift
final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure {
// …
func cancel() {
print("Canceling publisher")
subscriber = nil
job?.cancel(cause: nil)
}
}
Combine with error handling - Kotlin
private val counter = atomic(1)
fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin")
.onEach {
throw RuntimeException("Crash in Kotlin Flow")
}
.asCommonFlow()
Combine with error handling
public struct KotlinFlowError: Error { … }
public struct KotlinFlowPublisher<T: AnyObject>: Publisher {
public typealias Output = T
public typealias Failure = KotlinFlowError
...
job = flow.subscribe(
onEach: { el in subscriber.receive(el!) },
onComplete: { subscriber.receive(completion: .finished) },
onThrow: { (error: KotlinThrowable) in
let wrappedError = KotlinFlowError(error: error)
subscriber.receive(completion: .failure(wrappedError))
}
)
Combine with error handling - Kotlin
KotlinFlowPublisher<NSString>(flow: model.listToFlowThatThrows())
.sink(receiveCompletion: { error in
print("(String(describing: error))")
}, receiveValue: { (data) in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
})
.store(in: &jobs)
Combine with error handling - Kotlin
KotlinFlowPublisher<NSString>(flow: model.listToFlowThatThrows())
.sink(receiveCompletion: { error in
print("(String(describing: error))")
}, receiveValue: { (data) in
Swift.print("""
Receiving '(data)' on: 
(PlatformUtilsKt.threadId())
""")
})
.store(in: &jobs)
Main thread: 3606436
Sending 'Hello' on: 3606436
Receiving 'Hello' on: 3606436
Catching error:
kotlin.RuntimeException:
Canceling publisher
failure(iosApp.KotlinFlowError())
Flow complete
Async/Await
Task {
print("Start async/await Task on: (PlatformUtilsKt.threadId())")
let worker = KotlinWorker()
let result: KotlinObj = try! await worker.doWork()
print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())")
}
Async/Await
Task {
print("Start async/await Task on: (PlatformUtilsKt.threadId())")
let worker = KotlinWorker()
let result: KotlinObj = try! await worker.doWork()
print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())")
}
Main thread: 4417360
Start async/await Task on: 4417366
2021-10-16 15:45:02.216731+0200 iosApp[68736:4417366] *** Terminating app due to
uncaught exception 'NSGenericException', reason: 'Calling Kotlin suspend functions
from Swift/Objective-C is currently supported only on main thread'
Async/Await and controlling threads
func run() {
Task {
print("Start async/await Task on: (PlatformUtilsKt.threadId())")
let result: KotlinObj = try! await callWorker()
print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())")
}
}
@MainActor
func callWorker() async throws -> KotlinObj {
print("""Run Kotlin suspend function on: 
(PlatformUtilsKt.threadId())""")
let worker = KotlinWorker()
return try await worker.doWork()
}
Async/Await and controlling threads
func run() {
Task {
print("Start async/await Task on: (PlatformUtilsKt.threadId())")
let result: KotlinObj = try! await callWorker()
print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())")
}
}
@MainActor
func callWorker() async throws -> KotlinObj {
print("""Run Kotlin suspend function on: 
(PlatformUtilsKt.threadId())""")
let worker = KotlinWorker()
return try await worker.doWork()
}
Main thread: 4338526
Start async/await Task on: 4338847
Run Kotlin suspend function on:
4338526
Being called on: 4338526
Use result 'Hello from Kotlin' on:
4338526
Async/Await with SwiftUI
struct ContentView: View {
@StateObject var vm = MyViewModel()
var body: some View {
Text(vm.name).task {
await vm.doWork()
}
}
}
class MyViewModel: ObservableObject {
@Published var name: String = "-"
init() {}
let worker = PracticalCoroutines()
func doWork() async {
self.name = try! await worker.doWork().name
}
}
Summary
iOS Interop
● suspend functions are only callable on the Main
thread
● Kotlin methods must be marked with @Throws
● CoroutineScope cannot be controlled directly
from Swift
● All objects exposed in Swift should be frozen
● Custom callbacks are more flexible
● Events must manually be moved between iOS and
Kotlin
Summary
iOS Interop
● suspend functions are only callable on the Main
thread
● Kotlin methods must be marked with @Throws
● CoroutineScope cannot be controlled directly
from Swift
● All objects exposed in Swift should be frozen
● Custom callbacks are more flexible
● Events must manually be moved between iOS and
Kotlin
● Hopefully this talk will be obsolete by this time
next year
QA
Resources
● https://github.com/realm/realm-kotlin/
● https://github.com/realm/realm-kotlin-samples
● https://www.mongodb.com/realm
THANK YOU
@chrmelchior

More Related Content

PDF
Introduction to kotlin coroutines
PDF
Spring Boot
PDF
[PHP 也有 Day #64] PHP 升級指南
PDF
Introduction to Coroutines @ KotlinConf 2017
PDF
A quick and fast intro to Kotlin
PDF
Memory Management of C# with Unity Native Collections
PDF
Kotlin Coroutines Reloaded
PDF
UniRx - Reactive Extensions for Unity(EN)
Introduction to kotlin coroutines
Spring Boot
[PHP 也有 Day #64] PHP 升級指南
Introduction to Coroutines @ KotlinConf 2017
A quick and fast intro to Kotlin
Memory Management of C# with Unity Native Collections
Kotlin Coroutines Reloaded
UniRx - Reactive Extensions for Unity(EN)

What's hot (20)

PDF
Spring Boot
PPTX
Advanced JavaScript
PPTX
Writing and using Hamcrest Matchers
PDF
[Golang] 以 Mobile App 工程師視角,帶你進入 Golang 的世界 (Introduction of GoLang)
PPTX
React hooks
PDF
PDF
Deep Dive async/await in Unity with UniTask(EN)
PPTX
React workshop
PDF
Introduction to Kotlin coroutines
PDF
JUnit 5 - The Next Generation
PDF
Deep dive into Coroutines on JVM @ KotlinConf 2017
PPTX
Angular 2.0 forms
PDF
An Introduction to JUnit 5 and how to use it with Spring boot tests and Mockito
PDF
Advanced Reflection in Java
PDF
From Java 11 to 17 and beyond.pdf
PPTX
Java 8 Lambda and Streams
PDF
Testing Spring Boot Applications
PPTX
Coroutines in Kotlin
PPT
Android | Android Activity Launch Modes and Tasks | Gonçalo Silva
PPTX
Java Spring framework, Dependency Injection, DI, IoC, Inversion of Control
Spring Boot
Advanced JavaScript
Writing and using Hamcrest Matchers
[Golang] 以 Mobile App 工程師視角,帶你進入 Golang 的世界 (Introduction of GoLang)
React hooks
Deep Dive async/await in Unity with UniTask(EN)
React workshop
Introduction to Kotlin coroutines
JUnit 5 - The Next Generation
Deep dive into Coroutines on JVM @ KotlinConf 2017
Angular 2.0 forms
An Introduction to JUnit 5 and how to use it with Spring boot tests and Mockito
Advanced Reflection in Java
From Java 11 to 17 and beyond.pdf
Java 8 Lambda and Streams
Testing Spring Boot Applications
Coroutines in Kotlin
Android | Android Activity Launch Modes and Tasks | Gonçalo Silva
Java Spring framework, Dependency Injection, DI, IoC, Inversion of Control
Ad

Similar to Coroutines for Kotlin Multiplatform in Practise (20)

PDF
Kotlin wonderland
PPTX
A TypeScript Fans KotlinJS Adventures
PDF
Rapid Web API development with Kotlin and Ktor
PPT
Nodejs Intro Part One
PDF
droidcon Transylvania - Kotlin Coroutines
PPTX
Could Virtual Threads cast away the usage of Kotlin Coroutines - DevoxxUK2025
PDF
Koin Quickstart
PDF
Kotlin coroutine - the next step for RxJava developer?
PDF
Using the Groovy Ecosystem for Rapid JVM Development
PDF
KMM survival guide: how to tackle struggles between Kotlin and Swift
PDF
From Java to Kotlin - The first month in practice
PPTX
Android & Kotlin - The code awakens #01
PDF
Integration tests: use the containers, Luke!
PDF
New Features Of JDK 7
PDF
Android and the Seven Dwarfs from Devox'15
PPTX
DeadLock Preventer
PPTX
Could Virtual Threads cast away the usage of Kotlin Coroutines
PDF
Kotlin for Android - Vali Iorgu - mRready
PDF
Having Fun with Kotlin Android - DILo Surabaya
PPTX
Kotlin / Android Update
Kotlin wonderland
A TypeScript Fans KotlinJS Adventures
Rapid Web API development with Kotlin and Ktor
Nodejs Intro Part One
droidcon Transylvania - Kotlin Coroutines
Could Virtual Threads cast away the usage of Kotlin Coroutines - DevoxxUK2025
Koin Quickstart
Kotlin coroutine - the next step for RxJava developer?
Using the Groovy Ecosystem for Rapid JVM Development
KMM survival guide: how to tackle struggles between Kotlin and Swift
From Java to Kotlin - The first month in practice
Android & Kotlin - The code awakens #01
Integration tests: use the containers, Luke!
New Features Of JDK 7
Android and the Seven Dwarfs from Devox'15
DeadLock Preventer
Could Virtual Threads cast away the usage of Kotlin Coroutines
Kotlin for Android - Vali Iorgu - mRready
Having Fun with Kotlin Android - DILo Surabaya
Kotlin / Android Update
Ad

Recently uploaded (20)

PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
cuic standard and advanced reporting.pdf
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PPTX
Spectroscopy.pptx food analysis technology
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PPTX
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Dropbox Q2 2025 Financial Results & Investor Presentation
PPTX
Big Data Technologies - Introduction.pptx
PDF
Unlocking AI with Model Context Protocol (MCP)
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PPTX
Cloud computing and distributed systems.
PDF
Chapter 3 Spatial Domain Image Processing.pdf
DOCX
The AUB Centre for AI in Media Proposal.docx
PDF
Review of recent advances in non-invasive hemoglobin estimation
PPTX
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
PDF
Approach and Philosophy of On baking technology
PDF
Machine learning based COVID-19 study performance prediction
The Rise and Fall of 3GPP – Time for a Sabbatical?
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
cuic standard and advanced reporting.pdf
Advanced methodologies resolving dimensionality complications for autism neur...
Spectroscopy.pptx food analysis technology
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Dropbox Q2 2025 Financial Results & Investor Presentation
Big Data Technologies - Introduction.pptx
Unlocking AI with Model Context Protocol (MCP)
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
Cloud computing and distributed systems.
Chapter 3 Spatial Domain Image Processing.pdf
The AUB Centre for AI in Media Proposal.docx
Review of recent advances in non-invasive hemoglobin estimation
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
Approach and Philosophy of On baking technology
Machine learning based COVID-19 study performance prediction

Coroutines for Kotlin Multiplatform in Practise

  • 1. Coroutines for Kotlin Multiplatform in Practise Christian Melchior | Lead Engineer | MongoDB Realm | @chrmelchior
  • 2. Realm ● Open Source Object Database ● C++ with Language SDK’s on top ● First Android release in 2014 ● Part of MongoDB since 2019 ● Currently building a Kotlin Multiplatform SDK at https://github.com/realm/realm-kotlin/ By MongoDB
  • 3. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  • 4. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  • 5. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  • 6. Coroutines is a big topic Shared iOSApp AndroidApp JSApp Kotlin Common Constraints Dispatchers Memory model Testing Consuming Coroutines in Swift Completion Handlers Combine Async/Await
  • 8. Adding Coroutines - native-mt or not kotlin { sourceSets { commonMain { dependencies { // Choose one implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2-native-mt") } } } https://kotlinlang.org/docs/releases.html#release-details https://github.com/Kotlin/kotlinx.coroutines/issues/462
  • 9. native-mt standard Less bugs Only one thread on Kotlin Native Current standard Ktor ships with this Multithread support on Kotlin Native Will become the standard
  • 10. native-mt standard Less bugs Only one thread on Kotlin Native Current standard Ktor ships with this Multithread support on Kotlin Native Will become the standard
  • 11. Dispatchers val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main) viewScope.launch { val dbObject = withContext(Dispatchers.IO) { val networkObject = runNetworkRequest() writeToDB(networkObject) } val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) } updateUI(uiObject) }
  • 12. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 13. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 14. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 15. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 16. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 17. Unconfined Reuse parent thread Default JVM:Limited by CPUs available. Native: Single thread IO JVM Only Main Beware of Dragons Dispatchers Custom CoroutineDispatcher Integrate with framework run loops
  • 18. Dispatchers.Main val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main) viewScope.launch { val dbObject = withContext(Dispatchers.IO) { val networkObject = runNetworkRequest() writeToDB(networkObject) } val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) } updateUI(uiObject) }
  • 19. Dispatchers.Main val viewScope = CoroutineScope(CoroutineName("MyScope") + Dispatchers.Main) viewScope.launch { val dbObject = withContext(Dispatchers.IO) { val networkObject = runNetworkRequest() writeToDB(networkObject) } val uiObject = withContext(Dispatchers.Default) { UIObject.from(dbObject) } updateUI(uiObject) } iOS -> Deadlock JVM -> Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
  • 20. kotlin-coroutines-test val customMain = singleThreadDispatcher("CustomMainThread") Dispatchers.setMain(customMain) runBlockingTest { delay(100) doWork() } https://github.com/Kotlin/kotlinx.coroutines/issues/1996
  • 21. Inject Dispatchers expect object Platform { val MainDispatcher: CoroutineDispatcher val DefaultDispatcher: CoroutineDispatcher val UnconfinedDispatcher: CoroutineDispatcher val IODispatcher: CoroutineDispatcher } actual object Platform { actual val MainDispatcher get() = singleThreadDispatcher("CustomMainThread") actual val DefaultDispatcher get() = Dispatchers.Default actual val UnconfinedDispatcher get() = Dispatchers.Unconfined actual val IODispatcher get() = singleThreadDispatcher("CustomIOThread") }
  • 22. Inject Dispatchers val viewScope = CoroutineScope(CoroutineName("MyScope") + Platform.MainDispatcher) viewScope.launch { val dbObject = withContext(Platform.IODispatcher) { val networkObject = runNetworkRequest() writeToDB(networkObject) } val uiObject = withContext(Platform.DefaultDispatcher) { UIObject.from(dbObject) } updateUI(uiObject) }
  • 23. Advice #1: Always inject Dispatchers
  • 24. Kotlin Native Memory Model class MyObject { var name: String = "Jane Doe" var age: Int = 42 } Suspend fun automaticFreeze() { val obj = MyObject() withContext(Dispatchers.Default) { obj.name = "John Doe" } }
  • 25. Kotlin Native Memory Model class MyObject { var name: String = "Jane Doe" var age: Int = 42 } Suspend fun automaticFreeze() { val obj = MyObject() withContext(Dispatchers.Default) { obj.name = "John Doe" } } kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen io.realm.kotlin.practicalcoroutines.AllTests.MyObject@41a0a068
  • 26. Kotlin Native Memory Model class SafeMyObject { val name: AtomicRef<String> = atomic("Jane Doe") val age: AtomicInt = atomic(42) } fun nativeMemoryModel_safeAccess() { val obj = SafeMyObject() runBlocking(Dispatchers.Default) { obj.name.value = "Foo" } } https://github.com/Kotlin/kotlinx.atomicfu
  • 27. Freeze in constructors class SafeKotlinObj { val name: AtomicRef<String> = atomic("Jane Doe") val age: AtomicInt = atomic(42) init { this.freeze() } }
  • 28. Advice #2: Code and test against the most restrictive memory model
  • 29. Kotlin Common != Kotlin KMM public expect fun <T> runBlocking( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T public expect fun threadId(): String public expect fun singleThreadDispatcher(id: String): CoroutineDispatcher public expect fun <T> T.freeze(): T public expect val <T> T.isFrozen: Boolean public expect fun Any.ensureNeverFrozen()
  • 30. Kotlin Common != Kotlin KMM public expect fun <T> runBlocking( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T public expect fun threadId(): String public expect fun singleThreadDispatcher(id: String): CoroutineDispatcher public expect fun <T> T.freeze(): T public expect val <T> T.isFrozen: Boolean public expect fun Any.ensureNeverFrozen()
  • 31. Testing, RunBlocking and Deadlocks val dispatcher = singleThreadDispatcher("CustomThread") val otherDispatcher = singleThreadDispatcher("OtherThread") runBlocking(dispatcher) { runBlocking(otherDispatcher) { doWork() } }
  • 32. Testing, RunBlocking and Deadlocks val dispatcher = singleThreadDispatcher("CustomThread") runBlocking(dispatcher) { runBlocking(dispatcher) { doWork() } } Works on iOS Deadlocks on JVM
  • 34. Advice #3: Test with all dispatchers running on the same thread
  • 35. Advice #4: Test on both Native and JVM
  • 36. Summary ● Use native-mt. ● Always Inject Dispatchers. ● Avoid Dispatchers.Main in tests. ● Assume that all user defined Dispatchers are running on the same thread. ● KMM is not Kotlin Common. ● The frozen memory model also works on JVM. Write your shared code as for Kotlin Native. Shared Code
  • 38. Completion Handlers // Kotlin suspend fun doWork(): KotlinObj { println("Being called on: ${threadId()}") return KotlinObj() } // Swift print("Starting work on: (PlatformUtilsKt.threadId())") let model = KotlinModel() model.doWork() { (data: KotlinObj?, error: Error?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!.message) }
  • 39. Completion Handlers // Kotlin suspend fun doWork(): KotlinObj { println("Being called on: ${threadId()}") return KotlinObj() } // Swift print("Starting work on: (PlatformUtilsKt.threadId())") let model = KotlinModel() model.doWork() { (data: KotlinObj?, error: Error?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!.message) } Starting work from: 2183729 Being called on: 2183729 Result received on: 2183729 Hello from Kotlin
  • 40. Completion Handlers and Threads let model = KotlinModel() DispatchQueue.global(qos: .userInitiated).async { model.doWork() { data, error in print("Result received on: (PlatformUtilsKt.threadId())") } }
  • 41. Completion Handlers and Threads let model = KotlinModel() DispatchQueue.global(qos: .userInitiated).async { model.doWork() { data, error in print("Result received on: (PlatformUtilsKt.threadId())") } } Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared io.realm.kotlin.practicalcoroutines.PracticalCoroutines@814208 from other thread
  • 42. Advice #5: All public API’s should be frozen
  • 43. Completion Handlers and Threads let model = PlatformUtilsKt.freezeObject(obj: KotlinModel()) as! KotlinModel DispatchQueue.global(qos: .userInitiated).async { model.doWork() { data, error in print("Result received on: (PlatformUtilsKt.threadId())") } }
  • 44. Completion Handlers and Threads let model = PlatformUtilsKt.freezeObject(obj: KotlinModel()) as! KotlinModel DispatchQueue.global(qos: .userInitiated).async { model.doWork() { data, error in print("Result received on: (PlatformUtilsKt.threadId())") } } 2021-10-13 09:47:52.451417+0200 iosApp[83765:2103465] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Calling Kotlin suspend functions from Swift/Objective-C is currently supported only on main thread'
  • 45. Completion Handlers and Threads suspend fun doWorkInBackground(): KotlinObj { return withContext(Dispatchers.Default) { println("Being called on: ${threadId()}") KotlinObj() } } let model = KotlinModel() print("Starting on: (PlatformUtilsKt.threadId())") model.doWorkInBackground() { (data: KotlinObj?, error: Error?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!.message) }
  • 46. Completion Handlers and Threads suspend fun doWorkInBackground(): KotlinObj { return withContext(Dispatchers.Default) { println("Being called on: ${threadId()}") KotlinObj() } } let model = KotlinModel() print("Starting on: (PlatformUtilsKt.threadId())") model.doWorkInBackground() { (data: KotlinObj?, error: Error?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!.message) } Starting on: 29667 Being called on: 30896 Result received on: 29667 Hello from Kotlin
  • 47. Advice #6: Control Context in shared code
  • 48. Completion Handlers and error reporting suspend fun doWorkThatThrows(): KotlinObj { throw RuntimeException("Error from Kotlin") }
  • 49. Completion Handlers and error reporting suspend fun doWorkThatThrows(): KotlinObj { throw RuntimeException("Error from Kotlin") } Exception doesn't match @Throws-specified class list and thus isn't propagated from Kotlin to Objective-C/Swift as NSError. It is considered unexpected and unhandled instead. Program will be terminated. Uncaught Kotlin exception: kotlin.RuntimeException: Error from Kotlin
  • 50. Completion Handlers and error reporting @Throws(RuntimeException::class) suspend fun doWorkThatThrows(): KotlinObj { throw RuntimeException("Error from Kotlin") } model.doWorkThatThrows { (data: KotlinObj?, error: Error?) in if (error != nil) { handleError(error!) } else { handleResult(data!) } }
  • 51. Flows fun listenToFlow(): Flow<String> = flowOf("Hello", "from", "Kotlin") class Collector<T>: Kotlinx_coroutines_coreFlowCollector { let callback:(T) -> Void init(callback: @escaping (T) -> Void) { self.callback = callback } func emit(value: Any?, completionHandler: @escaping (KotlinUnit?, Error?) -> Void) { callback(value as! T) completionHandler(KotlinUnit(), nil) } } model.listenToFlow().collect(collector: Collector<String> { (data: String) in print(data) }) { (unit, error) in print("Done") } https://stackoverflow.com/questions/64175099/listen-to-kotlin-coroutine-flow-from-ios
  • 52. https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org /jetbrains/kotlinconf/FlowUtils.kt class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin { fun watch(block: (T) -> Unit): Closeable { val job = Job() onEach { block(it) }.launchIn(CoroutineScope(Dispatchers.Main + job)) return object : Closeable { override fun close() { job.cancel() } } } } internal fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this) fun listenToFlow (): CommonFlow<String> = flowOf("Hello", "from", "Kotlin").asCommonFlow().freeze() Flows
  • 53. Flows // Subscribe to flow let job: Closeable = model.listenToFlow().watch { (data: NSString?) in print(data!) } // Unsubscribe job.close()
  • 54. Flows // Subscribe to flow let job: Closeable = model.listenToFlow().watch { (data: NSString?) in print(data!) } Starting on: 2370807 Receiving on: 2370807 Hello from Kotlin
  • 55. Flows - Threading controlled from Kotlin print("Main thread: (PlatformUtilsKt.threadId())") DispatchQueue.global(qos: .userInitiated).async { print("This is run on a background queue: (PlatformUtilsKt.threadId())") KotlinModel().listenToFlow().watch { (data: NSString?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!) } }
  • 56. Flows - Threading controlled from Kotlin print("Main thread: (PlatformUtilsKt.threadId())") DispatchQueue.global(qos: .userInitiated).async { print("This is run on a background queue: (PlatformUtilsKt.threadId())") KotlinModel().listenToFlow().watch { (data: NSString?) in print("Result received on: (PlatformUtilsKt.threadId())") print(data!) } } Main thread: 2358850 This is run on a background queue: 2359090 Results received on: 2358850 Hello from Kotlin
  • 57. Flows - Threading controlled from Kotlin fun watch(block: (T) -> Unit): Closeable { val job = Job() onEach { block(it) }.launchIn(CoroutineScope(Dispatchers.Main + job)) return object : Closeable { override fun close() { job.cancel() } } } Main thread: 2358850 This is run on a background queue: 2359090 Results received on: 2358850 Hello from Kotlin
  • 58. Flows - Error handling fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin") .onEach { throw RuntimeException("Crash in Kotlin Flow") } .asCommonFlow() let model = KotlinModel() model.listToFlowThatThrows().watch { data in print(data!) }
  • 59. Flows - Error handling fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin") .onEach { throw RuntimeException("Crash in Kotlin Flow") } .asCommonFlow() let model = KotlinModel() model.listToFlowThatThrows().watch { data in print(data!) } Uncaught Kotlin exception: kotlin.Throwable: The process was terminated due to the unhandled exception thrown in the coroutine [StandaloneCoroutine{Cancelling}@2b852c8, MainDispatcher]: Crash in Kotlin Flow
  • 60. Flows - Error handling class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin { fun watch(block: (T?, Exception?) -> Unit): Closeable { val job = Job() onEach { block(it, null) } .catch { error: Throwable -> // Only pass on Exceptions. // This also correctly converts Exception to Swift Error. if (error is Exception) { block(null, error) } throw error // Then propagate exception on Kotlin side } .launchIn(CoroutineScope(Dispatchers.Main + job)) return object : Closeable { override fun close() { job.cancel() } } } }
  • 61. Flows - Error handling worker.listToFlowThatThrows().watch { (data: NSString?, error: KotlinException?) in if (error != nil) { print(error!.message!) } else { print(data!) } }
  • 62. Convert Flows to Combine public struct KotlinFlowPublisher<T: AnyObject>: Publisher { public typealias Output = T public typealias Failure = Never private let flow: CommonFlow<T> public init(flow: CommonFlow<T>) { self.flow = flow } public func receive<S: Subscriber>(subscriber: S) where S.Input == T, S.Failure == Failure { let subscription = KotlinFlowSubscription(flow: flow, subscriber: subscriber) subscriber.receive(subscription: subscription) } } https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/
  • 63. Convert Flows to Combine final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure { private var subscriber: S? private var job: Kotlinx_coroutines_coreJob? = nil private let flow: CommonFlow<T> init(flow: CommonFlow<T>, subscriber: S) { self.flow = flow self.subscriber = subscriber job = flow.subscribe( onEach: { el in subscriber.receive(el!) }, onComplete: { subscriber.receive(completion: .finished) }, onThrow: { error in debugPrint(error) } ) } func cancel() { subscriber = nil job?.cancel(cause: nil) } func request(_ demand: Subscribers.Demand) {} } }
  • 64. Convert Flows to Combine final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure { private var subscriber: S? private var job: Kotlinx_coroutines_coreJob? = nil private let flow: CommonFlow<T> init(flow: CommonFlow<T>, subscriber: S) { self.flow = flow self.subscriber = subscriber job = flow.subscribe( onEach: { el in subscriber.receive(el!) }, onComplete: { subscriber.receive(completion: .finished) }, onThrow: { error in debugPrint(error) } ) } func cancel() { subscriber = nil job?.cancel(cause: nil) } func request(_ demand: Subscribers.Demand) {} } }
  • 65. Convert Flows to Combine class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin { // ... // Expose Flow in a way that makes it possible to convert to Publisher in Swift. fun subscribe( onEach: (item: T) -> Unit, onComplete: () -> Unit, onThrow: (error: Throwable) -> Unit ) = this .onEach { onEach(it) } .catch { onThrow(it) } .onCompletion { onComplete() } .launchIn(CoroutineScope(Dispatchers.Main + job)) }
  • 66. Convert Flows to Combine - Usage KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs)
  • 67. Convert Flows to Combine - Usage KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs) Mainthread: 3392369 Sending 'Hello' on: 3392369 Sending 'from' on: 3392369 Sending 'Kotlin' on: 3392369 Receiving 'Hello' on: 3392369 Receiving 'from' on: 3392369 Receiving 'Kotlin' on: 3392369
  • 68. Controlling threads with Combine KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .subscribe(on: DispatchQueue.global()) .map { (data: NSString) -> String in print(""" Map (data) on: (PlatformUtilsKt.threadId()) """) return data as String } .receive(on: DispatchQueue.main) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs)
  • 69. Controlling threads with Combine KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .subscribe(on: DispatchQueue.global()) .map { (data: NSString) -> String in print(""" Map (data) on: (PlatformUtilsKt.threadId()) """) return data as String } .receive(on: DispatchQueue.main) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs) Mainthread: 3392369 Sending 'Hello' on: 3392369 Map 'Hello' on: 3392369 Sending 'from' on: 3392369 Map 'from' on: 3392369 Sending 'Kotlin' on: 3392369 Map 'Kotlin' on: 3392369 Receiving 'Hello' on: 3392369 Receiving 'from' on: 3392369 Receiving 'Kotlin' on: 3392369
  • 70. Controlling threads with Combine let serialBackgroundQueue = DispatchQueue.init(label: "background") KotlinFlowPublisher<NSString>(flow: worker.listenToFlow()) .subscribe(on: DispatchQueue.global()) .receive(on: serialBackgroundQueue) .map { (data: NSString) -> String in print(""" Map (data) on: (PlatformUtilsKt.threadId()) l """) return data as String } .receive(on: DispatchQueue.main) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs)
  • 71. Controlling threads with Combine let serialBackgroundQueue = DispatchQueue.init(label: "background") KotlinFlowPublisher<NSString>(flow: worker.listenToFlow()) .subscribe(on: DispatchQueue.global()) .receive(on: serialBackgroundQueue) .map { (data: NSString) -> String in print(""" Map (data) on: (PlatformUtilsKt.threadId()) """) return data as String } .receive(on: DispatchQueue.main) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs) Main thread: 3420316 Sending 'Hello' on: 3420316 Sending 'from' on: 3420316 Sending 'Kotlin' on: 3420316 Map Hello on: 3420430 Map from on: 3420430 Map Kotlin on: 3420430 Receiving 'Hello' on: 3420316 Receiving 'from' on: 3420316 Receiving 'Kotlin' on: 3420316
  • 72. Combine with error handling - Swift KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .tryMap { _ in throw DummyError() } .sink(receiveCompletion: { (error) in print("(String(describing: error))") }, receiveValue: { (data) in print(data) }) .store(in: &self.jobs)
  • 73. Combine with error handling - Swift KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .tryMap { _ in throw DummyError() } .sink(receiveCompletion: { (error) in print("(String(describing: error))") }, receiveValue: { (data) in print(data) }) .store(in: &self.jobs) Main thread: 3477630 Sending 'Hello' on: 3477630 Canceling publisher failure(iosApp.DummyError()) Sending 'from' on: 3477630 Sending 'Kotlin' on: 3477630 Flow complete
  • 74. Combine with error handling - Swift final class KotlinFlowSubscription<S: Subscriber>: Subscription where S.Input == T, S.Failure == Failure { // … func cancel() { print("Canceling publisher") subscriber = nil job?.cancel(cause: nil) } }
  • 75. Combine with error handling - Kotlin private val counter = atomic(1) fun listToFlowThatThrows(): CommonFlow<String> = flowOf("Hello", "from", "Kotlin") .onEach { throw RuntimeException("Crash in Kotlin Flow") } .asCommonFlow()
  • 76. Combine with error handling public struct KotlinFlowError: Error { … } public struct KotlinFlowPublisher<T: AnyObject>: Publisher { public typealias Output = T public typealias Failure = KotlinFlowError ... job = flow.subscribe( onEach: { el in subscriber.receive(el!) }, onComplete: { subscriber.receive(completion: .finished) }, onThrow: { (error: KotlinThrowable) in let wrappedError = KotlinFlowError(error: error) subscriber.receive(completion: .failure(wrappedError)) } )
  • 77. Combine with error handling - Kotlin KotlinFlowPublisher<NSString>(flow: model.listToFlowThatThrows()) .sink(receiveCompletion: { error in print("(String(describing: error))") }, receiveValue: { (data) in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) }) .store(in: &jobs)
  • 78. Combine with error handling - Kotlin KotlinFlowPublisher<NSString>(flow: model.listToFlowThatThrows()) .sink(receiveCompletion: { error in print("(String(describing: error))") }, receiveValue: { (data) in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) }) .store(in: &jobs) Main thread: 3606436 Sending 'Hello' on: 3606436 Receiving 'Hello' on: 3606436 Catching error: kotlin.RuntimeException: Canceling publisher failure(iosApp.KotlinFlowError()) Flow complete
  • 79. Async/Await Task { print("Start async/await Task on: (PlatformUtilsKt.threadId())") let worker = KotlinWorker() let result: KotlinObj = try! await worker.doWork() print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())") }
  • 80. Async/Await Task { print("Start async/await Task on: (PlatformUtilsKt.threadId())") let worker = KotlinWorker() let result: KotlinObj = try! await worker.doWork() print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())") } Main thread: 4417360 Start async/await Task on: 4417366 2021-10-16 15:45:02.216731+0200 iosApp[68736:4417366] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Calling Kotlin suspend functions from Swift/Objective-C is currently supported only on main thread'
  • 81. Async/Await and controlling threads func run() { Task { print("Start async/await Task on: (PlatformUtilsKt.threadId())") let result: KotlinObj = try! await callWorker() print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())") } } @MainActor func callWorker() async throws -> KotlinObj { print("""Run Kotlin suspend function on: (PlatformUtilsKt.threadId())""") let worker = KotlinWorker() return try await worker.doWork() }
  • 82. Async/Await and controlling threads func run() { Task { print("Start async/await Task on: (PlatformUtilsKt.threadId())") let result: KotlinObj = try! await callWorker() print("Use result '(result.name)' on: (PlatformUtilsKt.threadId())") } } @MainActor func callWorker() async throws -> KotlinObj { print("""Run Kotlin suspend function on: (PlatformUtilsKt.threadId())""") let worker = KotlinWorker() return try await worker.doWork() } Main thread: 4338526 Start async/await Task on: 4338847 Run Kotlin suspend function on: 4338526 Being called on: 4338526 Use result 'Hello from Kotlin' on: 4338526
  • 83. Async/Await with SwiftUI struct ContentView: View { @StateObject var vm = MyViewModel() var body: some View { Text(vm.name).task { await vm.doWork() } } } class MyViewModel: ObservableObject { @Published var name: String = "-" init() {} let worker = PracticalCoroutines() func doWork() async { self.name = try! await worker.doWork().name } }
  • 84. Summary iOS Interop ● suspend functions are only callable on the Main thread ● Kotlin methods must be marked with @Throws ● CoroutineScope cannot be controlled directly from Swift ● All objects exposed in Swift should be frozen ● Custom callbacks are more flexible ● Events must manually be moved between iOS and Kotlin
  • 85. Summary iOS Interop ● suspend functions are only callable on the Main thread ● Kotlin methods must be marked with @Throws ● CoroutineScope cannot be controlled directly from Swift ● All objects exposed in Swift should be frozen ● Custom callbacks are more flexible ● Events must manually be moved between iOS and Kotlin ● Hopefully this talk will be obsolete by this time next year