Successfully reported this slideshow.
Your SlideShare is downloading. ×

Coroutines for Kotlin Multiplatform in Practise

Upcoming SlideShare
Coroutines talk ppt
Coroutines talk ppt
Loading in …3
×

Check these out next

1 of 86 Ad
1 of 86 Ad

Coroutines for Kotlin Multiplatform in Practise

Download to read offline

Droidcon Berlin 2021 - With coroutines being the de facto way of exposing async work and streams of changes for Kotlin on Android, developers are obviously attempting to use the same approaches when moving their code to Multiplatform.

But due to the way the memory model differs between JVM and Kotlin Native, it can be a painful experience.

In this talk, we will take a deep dive into the Coroutine API for Kotlin Multiplatform. You will learn how to expose your API with Coroutines while working with the Kotlin Native memory model instead of against it, and avoid the dragons along the way.

Droidcon Berlin 2021 - With coroutines being the de facto way of exposing async work and streams of changes for Kotlin on Android, developers are obviously attempting to use the same approaches when moving their code to Multiplatform.

But due to the way the memory model differs between JVM and Kotlin Native, it can be a painful experience.

In this talk, we will take a deep dive into the Coroutine API for Kotlin Multiplatform. You will learn how to expose your API with Coroutines while working with the Kotlin Native memory model instead of against it, and avoid the dragons along the way.

Advertisement
Advertisement

More Related Content

Advertisement

Coroutines for Kotlin Multiplatform in Practise

  1. 1. Coroutines for Kotlin Multiplatform in Practise Christian Melchior | Lead Engineer | MongoDB Realm | @chrmelchior
  2. 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. 3. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  4. 4. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  5. 5. Coroutines is a big topic Shared iOSApp AndroidApp JSApp
  6. 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
  7. 7. Coroutines in Shared Code
  8. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 20. kotlin-coroutines-test val customMain = singleThreadDispatcher("CustomMainThread") Dispatchers.setMain(customMain) runBlockingTest { delay(100) doWork() } https://github.com/Kotlin/kotlinx.coroutines/issues/1996
  21. 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. 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. 23. Advice #1: Always inject Dispatchers
  24. 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. 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. 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. 27. Freeze in constructors class SafeKotlinObj { val name: AtomicRef<String> = atomic("Jane Doe") val age: AtomicInt = atomic(42) init { this.freeze() } }
  28. 28. Advice #2: Code and test against the most restrictive memory model
  29. 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. 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. 31. Testing, RunBlocking and Deadlocks val dispatcher = singleThreadDispatcher("CustomThread") val otherDispatcher = singleThreadDispatcher("OtherThread") runBlocking(dispatcher) { runBlocking(otherDispatcher) { doWork() } }
  32. 32. Testing, RunBlocking and Deadlocks val dispatcher = singleThreadDispatcher("CustomThread") runBlocking(dispatcher) { runBlocking(dispatcher) { doWork() } } Works on iOS Deadlocks on JVM
  33. 33. RunBlocking - Debugging
  34. 34. Advice #3: Test with all dispatchers running on the same thread
  35. 35. Advice #4: Test on both Native and JVM
  36. 36. Summary ● Use native-mt. ● Always Inject Dispatchers. ● Avoid Dispatchers.Main in tests. ● Assume that all user defined Dispatchers are running on the same thread. ● KMM is not Kotlin Common. ● The frozen memory model also works on JVM. Write your shared code as for Kotlin Native. Shared Code
  37. 37. Coroutines and Swift
  38. 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. 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. 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. 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. 42. Advice #5: All public API’s should be frozen
  43. 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. 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. 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. 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. 47. Advice #6: Control Context in shared code
  48. 48. Completion Handlers and error reporting suspend fun doWorkThatThrows(): KotlinObj { throw RuntimeException("Error from Kotlin") }
  49. 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. 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. 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. 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. 53. Flows // Subscribe to flow let job: Closeable = model.listenToFlow().watch { (data: NSString?) in print(data!) } // Unsubscribe job.close()
  54. 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. 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. 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. 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. 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. 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. 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. 61. Flows - Error handling worker.listToFlowThatThrows().watch { (data: NSString?, error: KotlinException?) in if (error != nil) { print(error!.message!) } else { print(data!) } }
  62. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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
  86. 86. QA Resources ● https://github.com/realm/realm-kotlin/ ● https://github.com/realm/realm-kotlin-samples ● https://www.mongodb.com/realm THANK YOU @chrmelchior

×