Slide 1

Slide 1 text

Swift Coroutines with KMP-NativeCoroutines Aleksander Jaworski @ FootballCo - 07.07.2023 Droidcon Berlin

Slide 2

Slide 2 text

Aleksander Jaworski - Android Developer @ FootballCo - Blog akjaw.com (Kotlin Multiplatform and Testing) - Twitter @akjaworski1

Slide 3

Slide 3 text

What is Kotlin Multiplatform?

Slide 4

Slide 4 text

What is Kotlin Multiplatform? - One codebase, different platforms (iOS, Desktop, Web)

Slide 5

Slide 5 text

What is Kotlin Multiplatform? - One codebase, different platforms (iOS, Desktop, Web) - Kotlin has the logic,platform has the UI* * Compose Multiplatform is a thing

Slide 6

Slide 6 text

How to call Kotlin from Swift? - Just call the generated Kotlin Multiplatform framework

Slide 7

Slide 7 text

How to call Kotlin from Swift? // Kotlin fun execute() { print("Exe") } - Just call the generated Kotlin Multiplatform framework

Slide 8

Slide 8 text

How to call Kotlin from Swift? // Kotlin fun execute() { print("Exe") } // Generated open func execute() - Just call the generated Kotlin Multiplatform framework

Slide 9

Slide 9 text

How to call Kotlin from Swift? // Kotlin fun execute() { print("Exe") } // Generated open func execute() // Swift execute() - Just call the generated Kotlin Multiplatform framework

Slide 10

Slide 10 text

How to call coroutines from Swift?

Slide 11

Slide 11 text

How to call coroutines from Swift? - It’s not so simple...

Slide 12

Slide 12 text

// Kotlin suspend fun execute() { print("44af1d53beeb") } How to call coroutines from Swift? - It’s not so simple...

Slide 13

Slide 13 text

// Generated * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. open func execute(completionHandler: @escaping (Error?) -> Void) // Kotlin suspend fun execute() { print("44af1d53beeb") } How to call coroutines from Swift? - It’s not so simple...

Slide 14

Slide 14 text

// Kotlin suspend fun execute() { print("44af1d53beeb") } // Swift execute { error in print(error) } // Generated * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. open func execute(completionHandler: @escaping (Error?) -> Void) How to call coroutines from Swift? - It’s not so simple...

Slide 15

Slide 15 text

// Generated * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. open func execute(completionHandler: @escaping (Error?) -> Void) // Swift execute { error in print(error) } // Kotlin suspend fun execute() { print("44af1d53beeb") } How to call coroutines from Swift? - It’s not so simple... - How to cancel?

Slide 16

Slide 16 text

// Generated * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. open func execute(completionHandler: @escaping (Error?) -> Void) // Swift execute { error in print(error) } // Kotlin suspend fun execute() { print("44af1d53beeb") } How to call coroutines from Swift? - It’s not so simple... - How to cancel? - Changing Threads?

Slide 17

Slide 17 text

- How to cancel? - Changing Threads? - Exception handling? // Generated * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. open func execute(completionHandler: @escaping (Error?) -> Void) // Swift execute { error in print(error) } // Kotlin suspend fun execute() { print("44af1d53beeb") } How to call coroutines from Swift? - It’s not so simple...

Slide 18

Slide 18 text

Agenda - Calling default Suspend and Flows directly from Swift - Creating Coroutine Adapters - Using KMP-NativeCoroutines - SwiftUI integrations - Exception Handling - Cancellation

Slide 19

Slide 19 text

The App - KaMPKit Starter from Touchlab - Downloads from API i and saves to a Database - 3 Coroutines call implementations

Slide 20

Slide 20 text

github.com/AKJAW/Swift-Coroutines-With-KMP-NativeCoroutines - KaMPKit Starter from Touchlab - Downloads from API i and saves to a Database - 3 Coroutines call implementations The App

Slide 21

Slide 21 text

What are we calling - Ordinary function - Suspend function - Data stream class BreedViewModel() : ViewModel() { val breedState: StateFlow suspend fun refreshBreeds(): Boolean fun updateBreedFavorite(breed: Breed): Job }

Slide 22

Slide 22 text

actual abstract class ViewModel { actual val viewModelScope = MainScope() fun clear() { viewModelScope.coroutineContext .cancelChildren() } } class BreedViewModel() : ViewModel() { val breedState: StateFlow suspend fun refreshBreeds(): Boolean fun updateBreedFavorite(breed: Breed): Job } What are we calling - Ordinary function - Suspend function - Data stream

Slide 23

Slide 23 text

The easiest one is the Ordinary function fun updateBreedFavorite(breed: Breed): Job { return viewModelScope.launch { breedRepository.updateBreedFavorite(breed) } }

Slide 24

Slide 24 text

fun updateBreedFavorite(breed: Breed): Job { return viewModelScope.launch { breedRepository.updateBreedFavorite(breed) } } func onBreedFavorite(_ breed: Breed) { viewModel?.updateBreedFavorite(breed: breed) } The easiest one is the Ordinary function

Slide 25

Slide 25 text

Ordinary functions - Fire and forget - Modify internal state - CoroutineScope is available fun updateBreedFavorite(breed: Breed): Job { return viewModelScope.launch { breedRepository.updateBreedFavorite(breed) } } func onBreedFavorite(_ breed: Breed) { viewModel?.updateBreedFavorite(breed: breed) }

Slide 26

Slide 26 text

Suspend functions

Slide 27

Slide 27 text

Suspending functions in Swift - Default generated framework

Slide 28

Slide 28 text

- Default generated framework - Manual Adaptera / Wrappera Suspending functions in Swift

Slide 29

Slide 29 text

- Default generated framework - Manual Adaptera / Wrappera - KMP-NativeCoroutines Plugin Suspending functions in Swift

Slide 30

Slide 30 text

Suspending functions - We want to know when it completes - The return value is needed - We want the ability to cancel it

Slide 31

Slide 31 text

suspend fun refreshBreeds(): Boolean { return try { breedRepository.refreshBreeds() true } catch (exception: Exception) { handleBreedError(exception) false } } Suspending functions - We want to know when it completes - The return value is needed - We want the ability to cancel it

Slide 32

Slide 32 text

Generated function - Cannot be cancelled - Forces an Optional value func refresh() { viewModel?.refreshBreeds { wasRefreshed, error in print("\(wasRefreshed), \(error)") } }

Slide 33

Slide 33 text

/** * @note This method converts instances of CancellationException to errors. * Other uncaught Kotlin exceptions are fatal. */ - (void)refreshBreedsWithCompletionHandler:(void (^)(SharedBoolean * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("refreshBreeds(completionHandler:)")) ); func refresh() { viewModel?.refreshBreeds { wasRefreshed, error in print("\(wasRefreshed), \(error)") } } Generated function - Cannot be cancelled - Forces an Optional value - No error handling

Slide 34

Slide 34 text

Adapter / Wrapper - Kotlin class SuspendAdapter( private val scope: CoroutineScope, private val suspender: suspend () -> T ) { fun subscribe( onSuccess: (item: T) -> Unit, onThrow: (error: Throwable) -> Unit ) = scope.launch { try { onSuccess(suspender()) } catch (error: Throwable) { onThrow(error) } } }

Slide 35

Slide 35 text

Adapter / Wrapper - Kotlin fun refreshBreeds() = SuspendAdapter(viewModel.scope) { viewModel.refreshBreeds() } class SuspendAdapter( private val scope: CoroutineScope, private val suspender: suspend () -> T ) { fun subscribe( onSuccess: (item: T) -> Unit, onThrow: (error: Throwable) -> Unit ) = scope.launch { try { onSuccess(suspender()) } catch (error: Throwable) { onThrow(error) } } }

Slide 36

Slide 36 text

Adapter / Wrapper - Kotlin class SuspendAdapter( private val scope: CoroutineScope, private val suspender: suspend () -> T ) { fun subscribe( onSuccess: (item: T) -> Unit, onThrow: (error: Throwable) -> Unit ) = scope.launch { try { onSuccess(suspender()) } catch (error: Throwable) { onThrow(error) } } } fun refreshBreeds() = SuspendAdapter(viewModel.scope) { viewModel.refreshBreeds() }

Slide 37

Slide 37 text

Adapter/Wrapper - Swift viewModel.refreshBreeds().subscribe( onSuccess: { value in print("completion \(value)") }, onThrow: { error in print("error \(error)") } ) - Better error handling - No Optional value - Additional boilerplate

Slide 38

Slide 38 text

Adapter/Wrapper - Swift - Better error handling - No Optional value - Additional boilerplate - Ability to integrate with Combine viewModel.refreshBreeds().subscribe( onSuccess: { value in print("completion \(value)") }, onThrow: { error in print("error \(error)") } ) createFuture(suspendAdapter: adapter) .sink { completion in print("completion \(completion)") } receiveValue: { value in print("recieveValue \(value)") }.store(in: &cancellables)

Slide 39

Slide 39 text

KMP-NativeCoroutines

Slide 40

Slide 40 text

Suspend KMP-NativeCoroutines @NativeCoroutines suspend fun nativeRefreshBreeds(): Boolean = refreshBreeds()

Slide 41

Slide 41 text

NativeCoroutines @NativeCoroutines suspend fun nativeRefreshBreeds(): Boolean = refreshBreeds() - No boilerplate

Slide 42

Slide 42 text

NativeCoroutines let suspend = viewModel.nativeRefreshBreeds() createFuture(for: suspend) .sink { completion in print("completion \(completion)") } receiveValue: { value in print("recieveValue \(value)") } @NativeCoroutines suspend fun nativeRefreshBreeds(): Boolean = refreshBreeds() - No boilerplate - Error handling - No Optional - Dedicated Swift packages Combine, Async, RxSwift

Slide 43

Slide 43 text

NativeCoroutines - No boilerplate - Error handling - No Optional - Dedicated Swift packages Combine, Async, RxSwift - Automatic cancelling let suspend = viewModel.nativeRefreshBreeds() createFuture(for: suspend) .sink { completion in print("completion \(completion)") } receiveValue: { value in print("recieveValue \(value)") }.store(in: &cancellables) @NativeCoroutines suspend fun nativeRefreshBreeds(): Boolean = refreshBreeds()

Slide 44

Slide 44 text

Flow Data Streams

Slide 45

Slide 45 text

Data streams - Generated framework - Many limitations + Boilerplate - Adapter / Wrapper - Additional boilerplate - KMP Native Coroutines

Slide 46

Slide 46 text

Flow Generated framework class Collector: Kotlinx_coroutines_coreFlowCollector { private let callback: (T) -> Void init(callback: @escaping (T) -> Void) { self.callback = callback } func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) { callback(value as! T) completionHandler(nil) } }

Slide 47

Slide 47 text

Flow Generated framework class Collector: Kotlinx_coroutines_coreFlowCollector { private let callback: (T) -> Void init(callback: @escaping (T) -> Void) { self.callback = callback } func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) { callback(value as! T) completionHandler(nil) } } viewModel.breedState.collect( collector: Collector { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error }, completionHandler: { error in print("breed collection completion error: \(error)") } )

Slide 48

Slide 48 text

Flow Adapter / Wrapper class FlowAdapter( private val scope: CoroutineScope, private val flow: Flow ) { fun subscribe( onEach: (item: T) -> Unit, onComplete: () -> Unit, onThrow: (error: Throwable) -> Unit ): Canceller = JobCanceller( flow.onEach { onEach(it) } .catch { onThrow(it) } .onCompletion { onComplete() } .launchIn(scope) ) } interface Canceller { fun cancel() } private class JobCanceller(private val job: Job) : Canceller { override fun cancel() { job.cancel() } }

Slide 49

Slide 49 text

Flow Adapter / Wrapper class FlowAdapter( private val scope: CoroutineScope, private val flow: Flow ) { fun subscribe( onEach: (item: T) -> Unit, onComplete: () -> Unit, onThrow: (error: Throwable) -> Unit ): Canceller = JobCanceller( flow.onEach { onEach(it) } .catch { onThrow(it) } .onCompletion { onComplete() } .launchIn(scope) ) } interface Canceller { fun cancel() } private class JobCanceller(private val job: Job) : Canceller { override fun cancel() { job.cancel() } } viewModel.breeds.subscribe( onEach: { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error }, onComplete: { print("Subscription end") }, onThrow: { error in print("Subscription error: \(error)") } )

Slide 50

Slide 50 text

Flow KMP-NativeCoroutines private val mutableBreedState = MutableStateFlow( BreedViewState(isLoading = true) ) @NativeCoroutinesState val nativeBreedState: StateFlow = breedState

Slide 51

Slide 51 text

Flow KMP-NativeCoroutines private val mutableBreedState = MutableStateFlow( BreedViewState(isLoading = true) ) @NativeCoroutinesState val nativeBreedState: StateFlow = breedState

Slide 52

Slide 52 text

Native Flow - Integration with Native solutions let nativeFlow = viewModel.nativeBreedStateFlow createPublisher(for: nativeFlow) .receive(on: DispatchQueue.main) .sink { completion in print("Breeds completion \(completion)") } receiveValue: { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error }

Slide 53

Slide 53 text

Native Flow let nativeFlow = viewModel.nativeBreedStateFlow createPublisher(for: nativeFlow) .receive(on: DispatchQueue.main) .sink { completion in print("Breeds completion \(completion)") } receiveValue: { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error } - Integration with Native solutions

Slide 54

Slide 54 text

Native Flow let nativeFlow = viewModel.nativeBreedStateFlow createPublisher(for: nativeFlow) .receive(on: DispatchQueue.main) .sink { completion in print("Breeds completion \(completion)") } receiveValue: { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error } - Integration with Native solutions - Collection on Default dispatcher

Slide 55

Slide 55 text

Native Flow - Integration with Native solutions - Collection on Default dispatcher - Automatic cancellation let nativeFlow = viewModel.nativeBreedStateFlow createPublisher(for: nativeFlow) .receive(on: DispatchQueue.main) .sink { completion in print("Breeds completion \(completion)") } receiveValue: { [weak self] dogsState in self?.loading = dogsState.isLoading self?.breeds = dogsState.breeds self?.error = dogsState.error } .store(in: &cancellables)

Slide 56

Slide 56 text

Controlling the Scope - Ability to cancel directly from Kotlin - Specify which thread should the suspension happen @NativeCoroutineScope actual val viewModelScope = MainScope()

Slide 57

Slide 57 text

SwiftUI

Slide 58

Slide 58 text

SwiftUI Integration - Combine - ObservableObject - @Published - @StateObject / @ObservedObject

Slide 59

Slide 59 text

Playground

Slide 60

Slide 60 text

Playground

Slide 61

Slide 61 text

Playground

Slide 62

Slide 62 text

Published Int @NativeCoroutines val numberFlow: Flow = flow { var i = 0 while (true) { emit(i++) delay(1000) } }

Slide 63

Slide 63 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 64

Slide 64 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 65

Slide 65 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 66

Slide 66 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 67

Slide 67 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 68

Slide 68 text

Published Int private class CoroutinesExampleModel: ObservableObject { @Published var number: Int = -1 func activate() { createPublisher(for: viewModel.numberFlow) .sink { _ in } receiveValue: { [weak self] number in self?.number = number.intValue } .store(in: &cancellables) } }

Slide 69

Slide 69 text

Published Int struct CoroutinesExampleScreen: View { @StateObject private var observableModel = CoroutinesExampleModel() var body: some View { Text("Number: \(observableModel.number)") } }

Slide 70

Slide 70 text

Published Int struct CoroutinesExampleScreen: View { @StateObject private var observableModel = CoroutinesExampleModel() var body: some View { Text("Number: \(observableModel.number)") } }

Slide 71

Slide 71 text

Published Int struct CoroutinesExampleScreen: View { @StateObject private var observableModel = CoroutinesExampleModel() var body: some View { Text("Number: \(observableModel.number)") } }

Slide 72

Slide 72 text

Published Int

Slide 73

Slide 73 text

KmmViewModel - Bonus - The same author - Generates ObservableObject itd.

Slide 74

Slide 74 text

KmmViewModel - Bonus ObservableObject KMMViewModel @StateObject @StateViewModel @ObservedObject @ObservedViewModel @EnvironmentObject @EnvironmentViewModel environmentObject(_:) environmentViewModel(_:) - The same author - Generates ObservableObject itd.

Slide 75

Slide 75 text

Suspend Exception Handling @NativeCoroutines suspend fun throwException() { delay(1000) throw IllegalStateException() }

Slide 76

Slide 76 text

Suspend Exception Handling print("future exception start") createFuture(for: viewModel.throwException()) .sink { completion in print("future exception completion \(completion)") } receiveValue: { value in }

Slide 77

Slide 77 text

Suspend Exception Handling 18:07:03 future exception start 18:07:04 future exception completion failure(Error Domain=KotlinExcepton Code=0 "(null)" UserInfo={KotlinException=kotlin.IllegalStateException}) print("future exception start") createFuture(for: viewModel.throwException()) .sink { completion in print("future exception completion \(completion)") } receiveValue: { value in }

Slide 78

Slide 78 text

Flow Exception Handling @NativeCoroutines val errorFlow: Flow = flow { repeat(3) { number -> emit(number) delay(1000) } throw IllegalStateException() }

Slide 79

Slide 79 text

Flow Exception Handling createPublisher(for: viewModel.errorFlow) .sink { completion in print("publisher exception completion \(completion)") } receiveValue: { number in print("publisher exception recieveValue \(number)") }

Slide 80

Slide 80 text

Flow Exception Handling createPublisher(for: viewModel.errorFlow) .sink { completion in print("publisher exception completion \(completion)") } receiveValue: { number in print("publisher exception recieveValue \(number)") } 2023-03-21 18:14:24 publisher exception recieveValue 0 2023-03-21 18:14:25 publisher exception recieveValue 1 2023-03-21 18:14:26 publisher exception recieveValue 2 2023-03-21 18:14:27 publisher exception completion failure(Error Domain=KotlinException Code=0 "(null)" UserInfo={KotlinException=kotlin.IllegalStateException})

Slide 81

Slide 81 text

Exception Handling - Exception is a Completion in Combine - Exception handling is hard in Swift - Better to return an error value @NativeCoroutines suspend fun throwException() { delay(1000) throw IllegalStateException() } @NativeCoroutines val errorFlow: Flow = flow { repeat(3) { number -> emit(number) delay(1000) } throw IllegalStateException() }

Slide 82

Slide 82 text

Cancellation @NativeCoroutines val numberFlow: Flow = flow { var i = 0 while (true) { emit(i++) delay(1000) } }.onEach { number -> log.i("numberFlow onEach: $number") }.onCompletion {throwable -> log.i("numberFlow onCompletion: $throwable") }

Slide 83

Slide 83 text

Manual cancellation createPublisher( for: viewModel.numberFlow ) .sink(...) .store(in: &cancellables)

Slide 84

Slide 84 text

Manual cancellation createPublisher( for: viewModel.numberFlow ) .sink(...) .store(in: &cancellables) func cancel() { cancellables.forEach { $0.cancel() } cancellables.removeAll() }

Slide 85

Slide 85 text

Manual cancellation createPublisher( for: viewModel.numberFlow ) .sink(...) .store(in: &cancellables) func cancel() { cancellables.forEach { $0.cancel() } cancellables.removeAll() } 2023-03-24 12:21:30 numberFlow onEach: 0 2023-03-24 12:21:31 numberFlow onEach: 1 2023-03-24 12:21:32 numberFlow onEach: 2 2023-03-24 12:21:33 numberFlow onCompletion: kotlinx.coroutines.JobCancellationException: …

Slide 86

Slide 86 text

Automatic cancellation

Slide 87

Slide 87 text

Automatic cancellation createPublisher( for: viewModel.numberFlow ) .sink(...) .store(in: &cancellables) 2023-03-24 12:28:13 init 0x44af1d53beeb 2023-03-24 12:28:13 numberFlow onEach: 0 2023-03-24 12:28:14 numberFlow onEach: 1 2023-03-24 12:28:15 numberFlow onEach: 2 2023-03-24 12:28:16 numberFlow onEach: 3 2023-03-24 12:28:16 deinit 0x44af1d53beeb 2023-03-24 12:28:16 numberFlow onCompletion: kotlinx.coroutines.JobCancellationException: ...

Slide 88

Slide 88 text

Cancellation - Freeing up resources - Avoiding unnecessary networking - Avoiding Memory leaks var cancellables = [AnyCancellable]() ... .store(in: &cancellables) func cancel() { cancellables.forEach { $0.cancel() } cancellables.removeAll() }

Slide 89

Slide 89 text

Summary

Slide 90

Slide 90 text

Summary - Different ways of Suspend and Flow from swift - Integrating KMP-NativeCoroutines with SwiftUi - Exception Handling - Cancellation

Slide 91

Slide 91 text

Learning Kotlin Multiplatform - https://johnoreilly.dev/ - https://touchlab.co/blog/ - https://www.marcogomiero.com/ - https://akjaw.com/

Slide 92

Slide 92 text

Sources - https://github.com/AKJAW/Swift-Coroutines-With-KMP-NativeCoroutines - https://github.com/touchlab/KaMPKit - https://github.com/rickclephas/KMP-NativeCoroutines - https://github.com/rickclephas/KMM-ViewModel - https://dev.to/touchlab/kotlin-coroutines-and-swift-revisited-j5h - https://www.slideshare.net/ChristianMelchior/coroutines-for-kotlin-multiplatform-in -practise

Slide 93

Slide 93 text

Questions?