SlideShare a Scribd company logo
1 of 86
Download to read offline
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

What's hot

What's hot (20)

Idiomatic Kotlin
Idiomatic KotlinIdiomatic Kotlin
Idiomatic Kotlin
 
Introduction to Kotlin for Android developers
Introduction to Kotlin for Android developersIntroduction to Kotlin for Android developers
Introduction to Kotlin for Android developers
 
Core Java - Quiz Questions - Bug Hunt
Core Java - Quiz Questions - Bug HuntCore Java - Quiz Questions - Bug Hunt
Core Java - Quiz Questions - Bug Hunt
 
Kotlin presentation
Kotlin presentation Kotlin presentation
Kotlin presentation
 
Data Persistence in Android with Room Library
Data Persistence in Android with Room LibraryData Persistence in Android with Room Library
Data Persistence in Android with Room Library
 
Kotlin InDepth Tutorial for beginners 2022
Kotlin InDepth Tutorial for beginners 2022Kotlin InDepth Tutorial for beginners 2022
Kotlin InDepth Tutorial for beginners 2022
 
Gitlab ci, cncf.sk
Gitlab ci, cncf.skGitlab ci, cncf.sk
Gitlab ci, cncf.sk
 
Introduction to kotlin for android app development gdg ahmedabad dev fest 2017
Introduction to kotlin for android app development   gdg ahmedabad dev fest 2017Introduction to kotlin for android app development   gdg ahmedabad dev fest 2017
Introduction to kotlin for android app development gdg ahmedabad dev fest 2017
 
Utilizing kotlin flows in an android application
Utilizing kotlin flows in an android applicationUtilizing kotlin flows in an android application
Utilizing kotlin flows in an android application
 
Asynchronous javascript
 Asynchronous javascript Asynchronous javascript
Asynchronous javascript
 
Android Development with Kotlin course
Android Development  with Kotlin courseAndroid Development  with Kotlin course
Android Development with Kotlin course
 
Gitlab ci-cd
Gitlab ci-cdGitlab ci-cd
Gitlab ci-cd
 
Analysing in depth work manager
Analysing in depth work managerAnalysing in depth work manager
Analysing in depth work manager
 
Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
 
Anatomy of a Spring Boot App with Clean Architecture - Spring I/O 2023
Anatomy of a Spring Boot App with Clean Architecture - Spring I/O 2023Anatomy of a Spring Boot App with Clean Architecture - Spring I/O 2023
Anatomy of a Spring Boot App with Clean Architecture - Spring I/O 2023
 
Unit Testing with Jest
Unit Testing with JestUnit Testing with Jest
Unit Testing with Jest
 
Introduction to Koltin for Android Part I
Introduction to Koltin for Android Part I Introduction to Koltin for Android Part I
Introduction to Koltin for Android Part I
 
Kotlin for Android Development
Kotlin for Android DevelopmentKotlin for Android Development
Kotlin for Android Development
 
Kotlin - Better Java
Kotlin - Better JavaKotlin - Better Java
Kotlin - Better Java
 
Dependency injection
Dependency injectionDependency injection
Dependency injection
 

Similar to Coroutines for Kotlin Multiplatform in Practise

Nanocloud cloud scale jvm
Nanocloud   cloud scale jvmNanocloud   cloud scale jvm
Nanocloud cloud scale jvm
aragozin
 

Similar to Coroutines for Kotlin Multiplatform in Practise (20)

Kotlin wonderland
Kotlin wonderlandKotlin wonderland
Kotlin wonderland
 
A TypeScript Fans KotlinJS Adventures
A TypeScript Fans KotlinJS AdventuresA TypeScript Fans KotlinJS Adventures
A TypeScript Fans KotlinJS Adventures
 
Rapid Web API development with Kotlin and Ktor
Rapid Web API development with Kotlin and KtorRapid Web API development with Kotlin and Ktor
Rapid Web API development with Kotlin and Ktor
 
Nodejs Intro Part One
Nodejs Intro Part OneNodejs Intro Part One
Nodejs Intro Part One
 
droidcon Transylvania - Kotlin Coroutines
droidcon Transylvania - Kotlin Coroutinesdroidcon Transylvania - Kotlin Coroutines
droidcon Transylvania - Kotlin Coroutines
 
Koin Quickstart
Koin QuickstartKoin Quickstart
Koin Quickstart
 
Kotlin coroutine - the next step for RxJava developer?
Kotlin coroutine - the next step for RxJava developer?Kotlin coroutine - the next step for RxJava developer?
Kotlin coroutine - the next step for RxJava developer?
 
Using the Groovy Ecosystem for Rapid JVM Development
Using the Groovy Ecosystem for Rapid JVM DevelopmentUsing the Groovy Ecosystem for Rapid JVM Development
Using the Groovy Ecosystem for Rapid JVM Development
 
From Java to Kotlin - The first month in practice
From Java to Kotlin - The first month in practiceFrom Java to Kotlin - The first month in practice
From Java to Kotlin - The first month in practice
 
Android & Kotlin - The code awakens #01
Android & Kotlin - The code awakens #01Android & Kotlin - The code awakens #01
Android & Kotlin - The code awakens #01
 
Integration tests: use the containers, Luke!
Integration tests: use the containers, Luke!Integration tests: use the containers, Luke!
Integration tests: use the containers, Luke!
 
New Features Of JDK 7
New Features Of JDK 7New Features Of JDK 7
New Features Of JDK 7
 
Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15
 
DeadLock Preventer
DeadLock PreventerDeadLock Preventer
DeadLock Preventer
 
Kotlin for Android - Vali Iorgu - mRready
Kotlin for Android - Vali Iorgu - mRreadyKotlin for Android - Vali Iorgu - mRready
Kotlin for Android - Vali Iorgu - mRready
 
Having Fun with Kotlin Android - DILo Surabaya
Having Fun with Kotlin Android - DILo SurabayaHaving Fun with Kotlin Android - DILo Surabaya
Having Fun with Kotlin Android - DILo Surabaya
 
Kotlin / Android Update
Kotlin / Android UpdateKotlin / Android Update
Kotlin / Android Update
 
Silicon Valley JUG: JVM Mechanics
Silicon Valley JUG: JVM MechanicsSilicon Valley JUG: JVM Mechanics
Silicon Valley JUG: JVM Mechanics
 
Nanocloud cloud scale jvm
Nanocloud   cloud scale jvmNanocloud   cloud scale jvm
Nanocloud cloud scale jvm
 
Kotlin/Everywhere GDG Bhubaneswar 2019
Kotlin/Everywhere GDG Bhubaneswar 2019 Kotlin/Everywhere GDG Bhubaneswar 2019
Kotlin/Everywhere GDG Bhubaneswar 2019
 

Recently uploaded

Structuring Teams and Portfolios for Success
Structuring Teams and Portfolios for SuccessStructuring Teams and Portfolios for Success
Structuring Teams and Portfolios for Success
UXDXConf
 

Recently uploaded (20)

Intro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджераIntro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджера
 
Structuring Teams and Portfolios for Success
Structuring Teams and Portfolios for SuccessStructuring Teams and Portfolios for Success
Structuring Teams and Portfolios for Success
 
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdfThe Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
 
TEST BANK For, Information Technology Project Management 9th Edition Kathy Sc...
TEST BANK For, Information Technology Project Management 9th Edition Kathy Sc...TEST BANK For, Information Technology Project Management 9th Edition Kathy Sc...
TEST BANK For, Information Technology Project Management 9th Edition Kathy Sc...
 
THE BEST IPTV in GERMANY for 2024: IPTVreel
THE BEST IPTV in  GERMANY for 2024: IPTVreelTHE BEST IPTV in  GERMANY for 2024: IPTVreel
THE BEST IPTV in GERMANY for 2024: IPTVreel
 
Connecting the Dots in Product Design at KAYAK
Connecting the Dots in Product Design at KAYAKConnecting the Dots in Product Design at KAYAK
Connecting the Dots in Product Design at KAYAK
 
Powerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara LaskowskaPowerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara Laskowska
 
FDO for Camera, Sensor and Networking Device – Commercial Solutions from VinC...
FDO for Camera, Sensor and Networking Device – Commercial Solutions from VinC...FDO for Camera, Sensor and Networking Device – Commercial Solutions from VinC...
FDO for Camera, Sensor and Networking Device – Commercial Solutions from VinC...
 
What's New in Teams Calling, Meetings and Devices April 2024
What's New in Teams Calling, Meetings and Devices April 2024What's New in Teams Calling, Meetings and Devices April 2024
What's New in Teams Calling, Meetings and Devices April 2024
 
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdfHow Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
 
The Metaverse: Are We There Yet?
The  Metaverse:    Are   We  There  Yet?The  Metaverse:    Are   We  There  Yet?
The Metaverse: Are We There Yet?
 
Where to Learn More About FDO _ Richard at FIDO Alliance.pdf
Where to Learn More About FDO _ Richard at FIDO Alliance.pdfWhere to Learn More About FDO _ Richard at FIDO Alliance.pdf
Where to Learn More About FDO _ Richard at FIDO Alliance.pdf
 
Top 10 Symfony Development Companies 2024
Top 10 Symfony Development Companies 2024Top 10 Symfony Development Companies 2024
Top 10 Symfony Development Companies 2024
 
10 Differences between Sales Cloud and CPQ, Blanka Doktorová
10 Differences between Sales Cloud and CPQ, Blanka Doktorová10 Differences between Sales Cloud and CPQ, Blanka Doktorová
10 Differences between Sales Cloud and CPQ, Blanka Doktorová
 
Optimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through ObservabilityOptimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through Observability
 
IESVE for Early Stage Design and Planning
IESVE for Early Stage Design and PlanningIESVE for Early Stage Design and Planning
IESVE for Early Stage Design and Planning
 
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi IbrahimzadeFree and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
 
Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024
 
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya HalderCustom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
Custom Approval Process: A New Perspective, Pavel Hrbacek & Anindya Halder
 
Strategic AI Integration in Engineering Teams
Strategic AI Integration in Engineering TeamsStrategic AI Integration in Engineering Teams
Strategic AI Integration in Engineering Teams
 

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