$30 off During Our Annual Pro Sale. View Details »

Troubled Waters: Bridging platform-native SDKs with Kotlin Multiplatform

Troubled Waters: Bridging platform-native SDKs with Kotlin Multiplatform

Talking to native APIs from Kotlin Multiplatform (mostly) Mobile.

Kevin Galligan

June 02, 2022
Tweet

More Decks by Kevin Galligan

Other Decks in Technology

Transcript

  1. Troubled Waters:
    Bridging platform-native SDKs
    with Kotlin Multiplatform
    Kevin Galligan

    View Slide

  2. Partner at
    Kotlin GDE

    View Slide

  3. Touchlab

    View Slide

  4. View Slide

  5. Common
    JVM JS Native

    View Slide

  6. Common
    JVM JS Native
    Firebase/JVM
    Firebase/JS
    Firebase/iOS

    View Slide

  7. View Slide

  8. Common
    JVM JS Native
    Firebase/JVM
    Firebase/JS
    Firebase/iOS

    View Slide

  9. Common
    Android iOS
    Firebase/JVM Firebase/iOS

    View Slide

  10. JNI

    View Slide

  11. Basic Topics
    • Tell Kotlin about the native code
    • Link native code into Kotlin Xcode Framework
    • API Design

    View Slide

  12. Simple First
    pass it in

    View Slide

  13. Interface Driven
    • 💰 Don’t need cinterop
    • 💰 No weird linking
    • 💰 No extra binary size
    • 🗑 Not easily testable
    • 🗑 Impractical for complex situations
    • 🗑 Bad for libraries

    View Slide

  14. interface KotlinAnalytics {
    fun logEvent(name: String, parameters: Map)
    }

    View Slide

  15. interface KotlinAnalytics {
    fun logEvent(name: String, parameters: Map)
    }
    fun initKoinIos(
    userDefaults: NSUserDefaults,
    appInfo: AppInfo,
    analytics: KotlinAnalytics,
    doOnStartup: () -> Unit
    ) (
    // Etc
    )

    View Slide

  16. class IosAnalytics: KotlinAnalytics {


    func logEvent(name: String, parameters: [String : Any]) {


    Analytics.logEvent(name, parameters: parameters)


    }


    }

    View Slide

  17. class IosAnalytics: KotlinAnalytics {


    func logEvent(name: String, parameters: [String : Any]) {


    Analytics.logEvent(name, parameters: parameters)


    }


    }
    KoinIOSKt.doInitKoinIos(


    userDefaults: userDefaults,


    appInfo: iosAppInfo,


    analytics: IosAnalytics(),


    doOnStartup: doOnStartup


    )

    View Slide

  18. fun withCallback(callback: (Breed) -> Unit) {
    }

    View Slide

  19. viewModel.withCallback(callback: {[weak self] (breed:Breed) -> Void in


    self?.callMe(breed: breed)


    })

    View Slide

  20. cinterop
    kotlin calling c/objc

    View Slide

  21. cinterop
    kotlin calling c/objc

    View Slide

  22. Call C/Obj-C
    some swift, some C++

    View Slide

  23. *.h cinterop Kotlin/iOS

    View Slide

  24. *.h cinterop Kotlin/iOS
    Binary
    ? ?
    ? ? ?
    ?
    ?
    ? ?? ?
    ?

    View Slide

  25. package = co.touchlab.whatever
    language = Objective-C
    headers = whatever.h
    whatever.def

    View Slide

  26. other config

    View Slide

  27. package = co.touchlab.whatever
    language = Objective-C
    headers = whatever.h
    whatever.def

    View Slide

  28. extraOpts = listOf("-mode", "sourcecode")

    View Slide

  29. *.h cinterop Kotlin/iOS
    Binary
    ? ?
    ? ? ?
    ?
    ?
    ? ?? ?
    ?

    View Slide

  30. Where are these *.h files?
    multiple options

    View Slide

  31. In Your Repo
    copy/paste

    View Slide

  32. View Slide

  33. Script Downloads
    grab headers

    View Slide

  34. Truncated Headers
    feels like cheating a bit :)

    View Slide

  35. View Slide

  36. Cocoapods Dependency
    the hammer approach

    View Slide

  37. kotlin {
    //etc
    cocoapods {
    summary = "Common library for the KaMP starter kit"
    homepage = "https://github.com/touchlab/KaMPKit"
    ios.deploymentTarget = "12.4"
    podfile = project.file("../ios/Podfile")
    pod("FirebaseAnalytics")
    }
    }

    View Slide

  38. Docs on Cocoapods/Kotlin
    https://kotlinlang.org/docs/native-cocoapods-libraries.html

    View Slide

  39. cocoapods.FirebaseAnalytics
    .FIRAnalytics.logEventWithName("someevent", mapOf(/*data*/))

    View Slide

  40. Makes project weird
    can’t run tests, dependency in podspec

    View Slide

  41. Carthage ❤ Kotlin
    https://github.com/wireapp/carthage-gradle-plugin

    View Slide

  42. SPM?
    not yet

    View Slide

  43. Linking
    where is the binary?

    View Slide

  44. *.h cinterop Kotlin/iOS
    Binary
    ? ?
    ? ? ?
    ?
    ?
    ? ?? ?
    ?

    View Slide

  45. complex-ish

    View Slide

  46. View Slide

  47. View Slide

  48. Linking Basics
    Binary
    Framework

    View Slide

  49. Static Linking
    Binary
    Static Framework

    View Slide

  50. Dynamic Linking
    Binary
    Dynamic Framework

    View Slide

  51. Assemble Static
    Static Framework
    *.h cinterop Kotlin/iOS
    Binary
    ? ?
    ? ? ?
    ?
    ?
    ??? ?
    ?

    View Slide

  52. Assemble Dynamic
    *.h cinterop Kotlin/iOS
    Binary
    ? ?
    ? ? ?
    ?
    ?
    ??? ?
    ?
    Dynamic Framework

    View Slide

  53. Libraries == More Complex
    recency bias

    View Slide

  54. Kermit
    Crashlytics
    Firebase.h
    Firebase

    View Slide

  55. Kermit
    Crashlytics
    Firebase.h
    Firebase
    Dynamic

    View Slide

  56. Undefined symbols for architecture x86_64:


    "_OBJC_CLASS_$_FIRStackFrame", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    "_OBJC_CLASS_$_FIRExceptionModel", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    "_OBJC_CLASS_$_FIRCrashlytics", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    ld: symbol(s) not found for architecture x86_64


    View Slide

  57. Undefined symbols for architecture x86_64:


    "_OBJC_CLASS_$_FIRStackFrame", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    "_OBJC_CLASS_$_FIRExceptionModel", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    "_OBJC_CLASS_$_FIRCrashlytics", referenced from:


    objc-class-ref in libco.touchlab:kermit-crashlytics-cache.a(result.o)


    ld: symbol(s) not found for architecture x86_64


    View Slide

  58. stay one lesson ahead…

    View Slide

  59. Kotlin/iOS
    Cocoapods
    *.h
    Binary

    View Slide

  60. Kotlin/iOS
    Some Code
    *.h
    *.c/*.m

    View Slide

  61. Kotlin/iOS
    *.h *.c/*.m
    cinterop clang

    View Slide

  62. Kotlin/iOS
    *.h *.c/*.m
    cinterop

    View Slide

  63. Kotlin/iOS
    *.h *.c/*.m
    cinterop cklib

    View Slide

  64. cklib Links
    • https://github.com/touchlab/cklib
    • https://github.com/cashapp/zipline/blob/trunk/
    zipline/build.gradle.kts#L157
    • https://github.com/touchlab/Kermit/blob/main/
    kermit-crashlytics-test/build.gradle.kts#L69

    View Slide

  65. Linking Links
    • https://bpoplauschi.github.io/2021/10/24/Intro-to-static-and-dynamic-
    libraries-frameworks.html
    • https://bpoplauschi.github.io/2021/10/25/Advanced-static-vs-dynamic-
    libraries-and-frameworks.html

    View Slide

  66. Testing
    Binary
    Framework

    View Slide

  67. Kotlin/iOS
    test.kexe

    View Slide

  68. Kotlin/iOS
    test.kexe
    Cocoapods
    *.h
    Binary Some Code
    *.h
    *.c/*.m

    View Slide

  69. Kotlin/iOS
    test.kexe
    *.h
    Binary
    Firebase.h
    Firebase

    View Slide

  70. Kotlin/iOS
    test.kexe
    *.h
    Binary
    Firebase.h
    Firebase
    Stub Binary

    View Slide

  71. View Slide

  72. Kotlin/iOS
    test.kexe
    *.h
    Binary
    Firebase.h
    Firebase
    Stub Binary

    View Slide

  73. stay one lesson ahead…

    View Slide

  74. API Design
    thoughts on the common language

    View Slide

  75. Uncanny Valley
    they do the same thing, but…

    View Slide

  76. Common
    Android iOS
    Firebase/JVM Firebase/iOS

    View Slide

  77. Common
    Android iOS
    Firebase/JVM Firebase/iOS

    View Slide

  78. Common
    Android iOS
    C code

    View Slide

  79. Common
    Android iOS
    C code
    JNI cinterop
    bridge

    View Slide

  80. Common
    Android iOS
    C code
    JNI cinterop
    bridge

    View Slide

  81. Rules are difficult
    “it depends” is everything

    View Slide

  82. Don’t default expect/actual
    can be good, can be frustrating

    View Slide

  83. expect/actual is less flexible
    harder to test, etc

    View Slide

  84. expect class KotlinAnalytics() {
    fun logEvent(name: String, parameters: Map)
    }

    View Slide

  85. interface KotlinAnalytics {
    fun logEvent(name: String, parameters: Map)
    }
    expect fun platformAnalyticsFactory(): KotlinAnalytics

    View Slide

  86. View Slide

  87. expect fun platformLogWriter(): LogWriter

    View Slide

  88. expect class CrashlyticsLogWriter(
    minSeverity: Severity = Severity.Info,
    minCrashSeverity: Severity = Severity.Warn,
    printTag: Boolean = true
    ) : LogWriter

    View Slide

  89. expect fun crashlyticsLogWriter(
    minSeverity: Severity = Severity.Info,
    minCrashSeverity: Severity = Severity.Warn,
    printTag: Boolean = true
    ) : LogWriter

    View Slide

  90. Service Objects/Small Graph
    interface and delegates work well

    View Slide

  91. Parent<->Child/Larger Graph
    interface-with-delegates starts looking ugly

    View Slide

  92. Order
    Big Graph/Data Objects
    getProducts() Product
    Product
    Product
    Supplier

    View Slide

  93. Order
    Big Graph/Data Objects
    getProducts() Product
    Product
    Product
    Supplier

    View Slide

  94. actual typealias
    with great power…

    View Slide

  95. /**


    * Multiplatform AtomicInt implementation


    */


    expect class AtomicInt(initialValue: Int) {


    fun get(): Int


    fun set(newValue: Int)


    fun incrementAndGet(): Int


    fun decrementAndGet(): Int


    fun addAndGet(delta: Int): Int


    fun compareAndSet(expected: Int, new: Int): Boolean


    }

    View Slide

  96. JVM Side?

    View Slide

  97. import java.util.concurrent.atomic.AtomicInteger


    actual typealias AtomicInt = AtomicInteger

    View Slide

  98. Actual needs to match
    because obviously it does

    View Slide

  99. import kotlin.native.concurrent.AtomicInt


    actual class AtomicInt actual constructor(initialValue:Int){


    private val atom = AtomicInt(initialValue)




    actual fun get(): Int = atom.value


    actual fun set(newValue: Int) {


    atom.value = newValue


    }


    actual fun incrementAndGet(): Int = atom.addAndGet(1)


    actual fun decrementAndGet(): Int = atom.addAndGet(-1)


    actual fun addAndGet(delta: Int): Int = atom.addAndGet(delta)


    actual fun compareAndSet(expected: Int, new: Int): Boolean =
    atom.compareAndSet(expected, new)


    }

    View Slide

  100. import kotlin.native.concurrent.AtomicInt


    actual class AtomicInt actual constructor(initialValue:Int){


    private val atom = AtomicInt(initialValue)




    actual fun get(): Int = atom.value


    actual fun set(newValue: Int) {


    atom.value = newValue


    }


    actual fun incrementAndGet(): Int = atom.addAndGet(1)


    actual fun decrementAndGet(): Int = atom.addAndGet(-1)


    actual fun addAndGet(delta: Int): Int = atom.addAndGet(delta)


    actual fun compareAndSet(expected: Int, new: Int): Boolean =
    atom.compareAndSet(expected, new)


    }

    View Slide

  101. Platform Affinity
    android iOS
    ?

    View Slide

  102. Platform Affinity
    android iOS
    android-y

    View Slide

  103. typealias can be brittle

    View Slide

  104. typealias can be brittle

    View Slide

  105. Platform Affinity
    android iOS
    android-y
    JS

    View Slide

  106. Parallel Delegates
    android iOS
    android-y

    View Slide

  107. Empty Typealias
    everything is extensions

    View Slide

  108. expect class QuerySnapshot


    expect val QuerySnapshot.documentChanges_:List


    expect fun QuerySnapshot.getDocumentChanges_(…):List


    expect val QuerySnapshot.documents_:List


    expect val QuerySnapshot.metadata: SnapshotMetadata


    expect val QuerySnapshot.query: Query


    expect val QuerySnapshot.empty: Boolean


    expect val QuerySnapshot.size: Int

    View Slide

  109. expect class QuerySnapshot


    expect val QuerySnapshot.documentChanges_:List


    expect fun QuerySnapshot.getDocumentChanges_(…):List


    expect val QuerySnapshot.documents_:List


    expect val QuerySnapshot.metadata: SnapshotMetadata


    expect val QuerySnapshot.query: Query


    expect val QuerySnapshot.empty: Boolean


    expect val QuerySnapshot.size: Int

    View Slide

  110. actual typealias QuerySnapshot = FIRQuerySnapshot


    actual val QuerySnapshot.documentChanges_: List


    get() = documentChanges as List


    actual val QuerySnapshot.documents_: List


    get() = documents as List


    actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata
    documentChangesWithIncludeMetadataChanges(metadataChanges == Metad
    iOS

    View Slide

  111. actual typealias QuerySnapshot = FIRQuerySnapshot


    actual val QuerySnapshot.documentChanges_: List


    get() = documentChanges as List


    actual val QuerySnapshot.documents_: List


    get() = documents as List


    actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata
    documentChangesWithIncludeMetadataChanges(metadataChanges == Metad
    iOS

    View Slide

  112. actual typealias QuerySnapshot = FIRQuerySnapshot


    actual val QuerySnapshot.documentChanges_: List


    get() = documentChanges as List


    actual val QuerySnapshot.documents_: List


    get() = documents as List


    actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata
    documentChangesWithIncludeMetadataChanges(metadataChanges == Metad
    iOS

    View Slide

  113. expect open class DocumentSnapshot
    DocumentSnapshot
    actual typealias DocumentSnapshot = FIRDocumentSnapshot
    iOS
    common
    actual typealias DocumentSnapshot =
    com.google.firebase.firestore.DocumentSnapshot
    Android

    View Slide

  114. actual typealias QuerySnapshot = com.google.firebase.firestore.QuerySn
    actual val QuerySnapshot.documentChanges_: List


    get() = documentChanges


    actual val QuerySnapshot.documents_: List


    get() = documents


    actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata
    getDocumentChanges(metadataChanges.toJvm())
    Android

    View Slide

  115. Platform Affinity

    View Slide

  116. Platform Agnostic

    View Slide

  117. Order
    Big Graph/Data Objects
    getProducts() Product
    Product
    Product
    Supplier

    View Slide

  118. Order
    Big Graph/Data Objects
    getProducts() Product
    Product
    Product
    Supplier

    View Slide

  119. Again, that’s rare
    mostly just use interfaces and delegates!

    View Slide

  120. Empty Typealias
    • https://vimeo.com/371460823
    • https://github.com/touchlab-lab/FirestoreKMP

    View Slide

  121. View Slide

  122. API *almost* designs itself
    you’re mapping existing APIs

    View Slide

  123. Iterative Design
    tweak for platform specifics

    View Slide

  124. Platform-Specific Init
    then common

    View Slide

  125. Whatever/
    Common
    Whatever/
    Android
    Whatever/
    iOS
    Whatever
    AAR Whatever.h
    Init Init

    View Slide

  126. Whatever/
    Common
    Whatever/
    Android
    Whatever/
    iOS
    Whatever
    AAR Whatever.h
    Init Init
    Other Calls

    View Slide

  127. Simple First
    pass it in

    View Slide

  128. That was a lot!
    it’s not all that bad

    View Slide

  129. At Touchlab
    • Big team build tools
    • Better iOS Dev ex
    • Swift code generation
    • Other things

    View Slide

  130. If you’re facing these issues…
    @kpgalligan
    Kotlin Slack

    View Slide

  131. Thanks!
    @kpgalligan

    View Slide

  132. Thanks!
    @kpgalligan
    Join the team
    !

    View Slide