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

More Related Content

What's hot

Fetch API Talk
Fetch API TalkFetch API Talk
Fetch API Talk
Chiamaka Nwolisa
 
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
Shengyou Fan
 
Introduction to kotlin coroutines
Introduction to kotlin coroutinesIntroduction to kotlin coroutines
Introduction to kotlin coroutines
NAVER Engineering
 
Jetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning TourJetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning Tour
Matthew Clarke
 
初探 Kotlin Multiplatform
初探 Kotlin Multiplatform初探 Kotlin Multiplatform
初探 Kotlin Multiplatform
Shengyou Fan
 
Kotlin Coroutines Reloaded
Kotlin Coroutines ReloadedKotlin Coroutines Reloaded
Kotlin Coroutines Reloaded
Roman Elizarov
 
Introduction to Coroutines @ KotlinConf 2017
Introduction to Coroutines @ KotlinConf 2017Introduction to Coroutines @ KotlinConf 2017
Introduction to Coroutines @ KotlinConf 2017
Roman Elizarov
 
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
Derek Lee Boire
 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with Spring
Joshua Long
 
Deep dive into Coroutines on JVM @ KotlinConf 2017
Deep dive into Coroutines on JVM @ KotlinConf 2017Deep dive into Coroutines on JVM @ KotlinConf 2017
Deep dive into Coroutines on JVM @ KotlinConf 2017
Roman Elizarov
 
Spring Boot
Spring BootSpring Boot
Spring Boot
Jiayun Zhou
 
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
Mintak Son
 
Jetpack Compose.pptx
Jetpack Compose.pptxJetpack Compose.pptx
Jetpack Compose.pptx
GDSCVJTI
 
ReactorKit으로 단방향 반응형 앱 만들기
ReactorKit으로 단방향 반응형 앱 만들기ReactorKit으로 단방향 반응형 앱 만들기
ReactorKit으로 단방향 반응형 앱 만들기
Suyeol Jeon
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modeling
Mario Fusco
 
Kotlin vs Java | Edureka
Kotlin vs Java | EdurekaKotlin vs Java | Edureka
Kotlin vs Java | Edureka
Edureka!
 
Jetpack compose
Jetpack composeJetpack compose
Jetpack compose
LutasLin
 
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
Shengyou Fan
 
Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI
Ajinkya Saswade
 
JavaScript Fetch API
JavaScript Fetch APIJavaScript Fetch API
JavaScript Fetch API
Xcat Liu
 

What's hot (20)

Fetch API Talk
Fetch API TalkFetch API Talk
Fetch API Talk
 
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
[JCConf 2023] 從 Kotlin Multiplatform 到 Compose Multiplatform:在多平台間輕鬆共用業務邏輯與 U...
 
Introduction to kotlin coroutines
Introduction to kotlin coroutinesIntroduction to kotlin coroutines
Introduction to kotlin coroutines
 
Jetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning TourJetpack Compose - A Lightning Tour
Jetpack Compose - A Lightning Tour
 
初探 Kotlin Multiplatform
初探 Kotlin Multiplatform初探 Kotlin Multiplatform
初探 Kotlin Multiplatform
 
Kotlin Coroutines Reloaded
Kotlin Coroutines ReloadedKotlin Coroutines Reloaded
Kotlin Coroutines Reloaded
 
Introduction to Coroutines @ KotlinConf 2017
Introduction to Coroutines @ KotlinConf 2017Introduction to Coroutines @ KotlinConf 2017
Introduction to Coroutines @ KotlinConf 2017
 
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
Why I ❤️ Kotlin Multiplatform (and want YOU to also ❤️ Kotlin Multiplatform)
 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with Spring
 
Deep dive into Coroutines on JVM @ KotlinConf 2017
Deep dive into Coroutines on JVM @ KotlinConf 2017Deep dive into Coroutines on JVM @ KotlinConf 2017
Deep dive into Coroutines on JVM @ KotlinConf 2017
 
Spring Boot
Spring BootSpring Boot
Spring Boot
 
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
LetSwift 2017 - 토스 iOS 앱의 개발/배포 환경
 
Jetpack Compose.pptx
Jetpack Compose.pptxJetpack Compose.pptx
Jetpack Compose.pptx
 
ReactorKit으로 단방향 반응형 앱 만들기
ReactorKit으로 단방향 반응형 앱 만들기ReactorKit으로 단방향 반응형 앱 만들기
ReactorKit으로 단방향 반응형 앱 만들기
 
From object oriented to functional domain modeling
From object oriented to functional domain modelingFrom object oriented to functional domain modeling
From object oriented to functional domain modeling
 
Kotlin vs Java | Edureka
Kotlin vs Java | EdurekaKotlin vs Java | Edureka
Kotlin vs Java | Edureka
 
Jetpack compose
Jetpack composeJetpack compose
Jetpack compose
 
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
以 Kotlin Multiplatform Mobile (KMM) 開發跨平台行動應用
 
Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI Android jetpack compose | Declarative UI
Android jetpack compose | Declarative UI
 
JavaScript Fetch API
JavaScript Fetch APIJavaScript Fetch API
JavaScript Fetch API
 

Similar to Coroutines for Kotlin Multiplatform in Practise

Kotlin wonderland
Kotlin wonderlandKotlin wonderland
Kotlin wonderland
Jedsada Tiwongvokul
 
A TypeScript Fans KotlinJS Adventures
A TypeScript Fans KotlinJS AdventuresA TypeScript Fans KotlinJS Adventures
A TypeScript Fans KotlinJS Adventures
Garth Gilmour
 
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
Trayan Iliev
 
Nodejs Intro Part One
Nodejs Intro Part OneNodejs Intro Part One
Nodejs Intro Part One
Budh Ram Gurung
 
droidcon Transylvania - Kotlin Coroutines
droidcon Transylvania - Kotlin Coroutinesdroidcon Transylvania - Kotlin Coroutines
droidcon Transylvania - Kotlin Coroutines
Arthur Nagy
 
Koin Quickstart
Koin QuickstartKoin Quickstart
Koin Quickstart
Matthew Clarke
 
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?
Artur Latoszewski
 
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
Schalk Cronjé
 
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
StefanTomm
 
Android & Kotlin - The code awakens #01
Android & Kotlin - The code awakens #01Android & Kotlin - The code awakens #01
Android & Kotlin - The code awakens #01
Omar Miatello
 
Integration tests: use the containers, Luke!
Integration tests: use the containers, Luke!Integration tests: use the containers, Luke!
Integration tests: use the containers, Luke!
Roberto Franchini
 
New Features Of JDK 7
New Features Of JDK 7New Features Of JDK 7
New Features Of JDK 7
Deniz Oguz
 
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
Murat Yener
 
DeadLock Preventer
DeadLock PreventerDeadLock Preventer
DeadLock Preventer
Teodor Madan
 
Kotlin for Android - Vali Iorgu - mRready
Kotlin for Android - Vali Iorgu - mRreadyKotlin for Android - Vali Iorgu - mRready
Kotlin for Android - Vali Iorgu - mRready
MobileAcademy
 
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
DILo Surabaya
 
Kotlin / Android Update
Kotlin / Android UpdateKotlin / Android Update
Kotlin / Android Update
Garth Gilmour
 
Silicon Valley JUG: JVM Mechanics
Silicon Valley JUG: JVM MechanicsSilicon Valley JUG: JVM Mechanics
Silicon Valley JUG: JVM Mechanics
Azul Systems, Inc.
 
Nanocloud cloud scale jvm
Nanocloud   cloud scale jvmNanocloud   cloud scale jvm
Nanocloud cloud scale jvmaragozin
 
Kotlin/Everywhere GDG Bhubaneswar 2019
Kotlin/Everywhere GDG Bhubaneswar 2019 Kotlin/Everywhere GDG Bhubaneswar 2019
Kotlin/Everywhere GDG Bhubaneswar 2019
Sriyank Siddhartha
 

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

GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
Neo4j
 
20240609 QFM020 Irresponsible AI Reading List May 2024
20240609 QFM020 Irresponsible AI Reading List May 202420240609 QFM020 Irresponsible AI Reading List May 2024
20240609 QFM020 Irresponsible AI Reading List May 2024
Matthew Sinclair
 
A tale of scale & speed: How the US Navy is enabling software delivery from l...
A tale of scale & speed: How the US Navy is enabling software delivery from l...A tale of scale & speed: How the US Navy is enabling software delivery from l...
A tale of scale & speed: How the US Navy is enabling software delivery from l...
sonjaschweigert1
 
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdfObservability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
Paige Cruz
 
Microsoft - Power Platform_G.Aspiotis.pdf
Microsoft - Power Platform_G.Aspiotis.pdfMicrosoft - Power Platform_G.Aspiotis.pdf
Microsoft - Power Platform_G.Aspiotis.pdf
Uni Systems S.M.S.A.
 
Generative AI Deep Dive: Advancing from Proof of Concept to Production
Generative AI Deep Dive: Advancing from Proof of Concept to ProductionGenerative AI Deep Dive: Advancing from Proof of Concept to Production
Generative AI Deep Dive: Advancing from Proof of Concept to Production
Aggregage
 
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
DanBrown980551
 
Climate Impact of Software Testing at Nordic Testing Days
Climate Impact of Software Testing at Nordic Testing DaysClimate Impact of Software Testing at Nordic Testing Days
Climate Impact of Software Testing at Nordic Testing Days
Kari Kakkonen
 
Elizabeth Buie - Older adults: Are we really designing for our future selves?
Elizabeth Buie - Older adults: Are we really designing for our future selves?Elizabeth Buie - Older adults: Are we really designing for our future selves?
Elizabeth Buie - Older adults: Are we really designing for our future selves?
Nexer Digital
 
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptx
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptxSecstrike : Reverse Engineering & Pwnable tools for CTF.pptx
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptx
nkrafacyberclub
 
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
James Anderson
 
GridMate - End to end testing is a critical piece to ensure quality and avoid...
GridMate - End to end testing is a critical piece to ensure quality and avoid...GridMate - End to end testing is a critical piece to ensure quality and avoid...
GridMate - End to end testing is a critical piece to ensure quality and avoid...
ThomasParaiso2
 
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
Albert Hoitingh
 
GraphSummit Singapore | The Art of the Possible with Graph - Q2 2024
GraphSummit Singapore | The Art of the  Possible with Graph - Q2 2024GraphSummit Singapore | The Art of the  Possible with Graph - Q2 2024
GraphSummit Singapore | The Art of the Possible with Graph - Q2 2024
Neo4j
 
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
Neo4j
 
Large Language Model (LLM) and it’s Geospatial Applications
Large Language Model (LLM) and it’s Geospatial ApplicationsLarge Language Model (LLM) and it’s Geospatial Applications
Large Language Model (LLM) and it’s Geospatial Applications
Rohit Gautam
 
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex ProofszkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
Alex Pruden
 
DevOps and Testing slides at DASA Connect
DevOps and Testing slides at DASA ConnectDevOps and Testing slides at DASA Connect
DevOps and Testing slides at DASA Connect
Kari Kakkonen
 
UiPath Test Automation using UiPath Test Suite series, part 5
UiPath Test Automation using UiPath Test Suite series, part 5UiPath Test Automation using UiPath Test Suite series, part 5
UiPath Test Automation using UiPath Test Suite series, part 5
DianaGray10
 
Communications Mining Series - Zero to Hero - Session 1
Communications Mining Series - Zero to Hero - Session 1Communications Mining Series - Zero to Hero - Session 1
Communications Mining Series - Zero to Hero - Session 1
DianaGray10
 

Recently uploaded (20)

GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
GraphSummit Singapore | Enhancing Changi Airport Group's Passenger Experience...
 
20240609 QFM020 Irresponsible AI Reading List May 2024
20240609 QFM020 Irresponsible AI Reading List May 202420240609 QFM020 Irresponsible AI Reading List May 2024
20240609 QFM020 Irresponsible AI Reading List May 2024
 
A tale of scale & speed: How the US Navy is enabling software delivery from l...
A tale of scale & speed: How the US Navy is enabling software delivery from l...A tale of scale & speed: How the US Navy is enabling software delivery from l...
A tale of scale & speed: How the US Navy is enabling software delivery from l...
 
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdfObservability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
Observability Concepts EVERY Developer Should Know -- DeveloperWeek Europe.pdf
 
Microsoft - Power Platform_G.Aspiotis.pdf
Microsoft - Power Platform_G.Aspiotis.pdfMicrosoft - Power Platform_G.Aspiotis.pdf
Microsoft - Power Platform_G.Aspiotis.pdf
 
Generative AI Deep Dive: Advancing from Proof of Concept to Production
Generative AI Deep Dive: Advancing from Proof of Concept to ProductionGenerative AI Deep Dive: Advancing from Proof of Concept to Production
Generative AI Deep Dive: Advancing from Proof of Concept to Production
 
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
LF Energy Webinar: Electrical Grid Modelling and Simulation Through PowSyBl -...
 
Climate Impact of Software Testing at Nordic Testing Days
Climate Impact of Software Testing at Nordic Testing DaysClimate Impact of Software Testing at Nordic Testing Days
Climate Impact of Software Testing at Nordic Testing Days
 
Elizabeth Buie - Older adults: Are we really designing for our future selves?
Elizabeth Buie - Older adults: Are we really designing for our future selves?Elizabeth Buie - Older adults: Are we really designing for our future selves?
Elizabeth Buie - Older adults: Are we really designing for our future selves?
 
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptx
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptxSecstrike : Reverse Engineering & Pwnable tools for CTF.pptx
Secstrike : Reverse Engineering & Pwnable tools for CTF.pptx
 
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
Alt. GDG Cloud Southlake #33: Boule & Rebala: Effective AppSec in SDLC using ...
 
GridMate - End to end testing is a critical piece to ensure quality and avoid...
GridMate - End to end testing is a critical piece to ensure quality and avoid...GridMate - End to end testing is a critical piece to ensure quality and avoid...
GridMate - End to end testing is a critical piece to ensure quality and avoid...
 
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
Encryption in Microsoft 365 - ExpertsLive Netherlands 2024
 
GraphSummit Singapore | The Art of the Possible with Graph - Q2 2024
GraphSummit Singapore | The Art of the  Possible with Graph - Q2 2024GraphSummit Singapore | The Art of the  Possible with Graph - Q2 2024
GraphSummit Singapore | The Art of the Possible with Graph - Q2 2024
 
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
GraphSummit Singapore | Neo4j Product Vision & Roadmap - Q2 2024
 
Large Language Model (LLM) and it’s Geospatial Applications
Large Language Model (LLM) and it’s Geospatial ApplicationsLarge Language Model (LLM) and it’s Geospatial Applications
Large Language Model (LLM) and it’s Geospatial Applications
 
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex ProofszkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
zkStudyClub - Reef: Fast Succinct Non-Interactive Zero-Knowledge Regex Proofs
 
DevOps and Testing slides at DASA Connect
DevOps and Testing slides at DASA ConnectDevOps and Testing slides at DASA Connect
DevOps and Testing slides at DASA Connect
 
UiPath Test Automation using UiPath Test Suite series, part 5
UiPath Test Automation using UiPath Test Suite series, part 5UiPath Test Automation using UiPath Test Suite series, part 5
UiPath Test Automation using UiPath Test Suite series, part 5
 
Communications Mining Series - Zero to Hero - Session 1
Communications Mining Series - Zero to Hero - Session 1Communications Mining Series - Zero to Hero - Session 1
Communications Mining Series - Zero to Hero - Session 1
 

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