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