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

Coroutines for Kotlin Multiplatform in Practise

  • 1.
    Coroutines for Kotlin Multiplatformin Practise Christian Melchior | Lead Engineer | MongoDB Realm | @chrmelchior
  • 2.
    Realm ● Open SourceObject 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 abig topic Shared iOSApp AndroidApp JSApp
  • 4.
    Coroutines is abig topic Shared iOSApp AndroidApp JSApp
  • 5.
    Coroutines is abig topic Shared iOSApp AndroidApp JSApp
  • 6.
    Coroutines is abig topic Shared iOSApp AndroidApp JSApp Kotlin Common Constraints Dispatchers Memory model Testing Consuming Coroutines in Swift Completion Handlers Combine Async/Await
  • 7.
  • 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 onethread 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 onethread 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:Limitedby 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:Limitedby 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:Limitedby 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:Limitedby 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:Limitedby 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:Limitedby 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 objectPlatform { 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: Alwaysinject Dispatchers
  • 24.
    Kotlin Native MemoryModel 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 MemoryModel 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 MemoryModel 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 classSafeKotlinObj { val name: AtomicRef<String> = atomic("Jane Doe") val age: AtomicInt = atomic(42) init { this.freeze() } }
  • 28.
    Advice #2: Codeand 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 andDeadlocks val dispatcher = singleThreadDispatcher("CustomThread") val otherDispatcher = singleThreadDispatcher("OtherThread") runBlocking(dispatcher) { runBlocking(otherDispatcher) { doWork() } }
  • 32.
    Testing, RunBlocking andDeadlocks val dispatcher = singleThreadDispatcher("CustomThread") runBlocking(dispatcher) { runBlocking(dispatcher) { doWork() } } Works on iOS Deadlocks on JVM
  • 33.
  • 34.
    Advice #3: Testwith all dispatchers running on the same thread
  • 35.
    Advice #4: Teston 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
  • 37.
  • 38.
    Completion Handlers // Kotlin suspendfun 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 suspendfun 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 andThreads let model = KotlinModel() DispatchQueue.global(qos: .userInitiated).async { model.doWork() { data, error in print("Result received on: (PlatformUtilsKt.threadId())") } }
  • 41.
    Completion Handlers andThreads 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: Allpublic API’s should be frozen
  • 43.
    Completion Handlers andThreads 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 andThreads 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 andThreads 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 andThreads 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.
  • 48.
    Completion Handlers anderror reporting suspend fun doWorkThatThrows(): KotlinObj { throw RuntimeException("Error from Kotlin") }
  • 49.
    Completion Handlers anderror 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 anderror 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 valorigin: 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 toflow let job: Closeable = model.listenToFlow().watch { (data: NSString?) in print(data!) } // Unsubscribe job.close()
  • 54.
    Flows // Subscribe toflow let job: Closeable = model.listenToFlow().watch { (data: NSString?) in print(data!) } Starting on: 2370807 Receiving on: 2370807 Hello from Kotlin
  • 55.
    Flows - Threadingcontrolled 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 - Threadingcontrolled 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 - Threadingcontrolled 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 - Errorhandling 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 - Errorhandling 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 - Errorhandling 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 - Errorhandling worker.listToFlowThatThrows().watch { (data: NSString?, error: KotlinException?) in if (error != nil) { print(error!.message!) } else { print(data!) } }
  • 62.
    Convert Flows toCombine 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 toCombine 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 toCombine 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 toCombine 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 toCombine - Usage KotlinFlowPublisher<NSString>(flow: model.listenToFlow()) .sink { data in Swift.print(""" Receiving '(data)' on: (PlatformUtilsKt.threadId()) """) } .store(in: &self.jobs)
  • 67.
    Convert Flows toCombine - 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 withCombine 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 withCombine 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 withCombine 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 withCombine 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 errorhandling - 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 errorhandling - 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 errorhandling - 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 errorhandling - 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 errorhandling 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 errorhandling - 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 errorhandling - 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/awaitTask 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/awaitTask 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 controllingthreads 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 controllingthreads 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 structContentView: 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 ● suspendfunctions 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 ● suspendfunctions 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
  • 86.