$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

  2. Partner at Kotlin GDE

  3. Touchlab

  4. None
  5. Common JVM JS Native

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

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

  9. Common Android iOS Firebase/JVM Firebase/iOS

  10. JNI

  11. Basic Topics • Tell Kotlin about the native code •

    Link native code into Kotlin Xcode Framework • API Design
  12. Simple First pass it in

  13. Interface Driven • 💰 Don’t need cinterop • 💰 No

    weird linking • 💰 No extra binary size • 🗑 Not easily testable • 🗑 Impractical for complex situations • 🗑 Bad for libraries
  14. interface KotlinAnalytics { fun logEvent(name: String, parameters: Map<String, Any>) }

  15. interface KotlinAnalytics { fun logEvent(name: String, parameters: Map<String, Any>) }

    fun initKoinIos( userDefaults: NSUserDefaults, appInfo: AppInfo, analytics: KotlinAnalytics, doOnStartup: () -> Unit ) ( // Etc )
  16. class IosAnalytics: KotlinAnalytics { func logEvent(name: String, parameters: [String :

    Any]) { Analytics.logEvent(name, parameters: parameters) } }
  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 )
  18. fun withCallback(callback: (Breed) -> Unit) { }

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

  20. cinterop kotlin calling c/objc

  21. cinterop kotlin calling c/objc

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

  23. *.h cinterop Kotlin/iOS

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

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

  26. other config

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

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

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

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

  31. In Your Repo copy/paste

  32. None
  33. Script Downloads grab headers

  34. Truncated Headers feels like cheating a bit :)

  35. None
  36. Cocoapods Dependency the hammer approach

  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") } }
  38. Docs on Cocoapods/Kotlin https://kotlinlang.org/docs/native-cocoapods-libraries.html

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

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

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

  42. SPM? not yet

  43. Linking where is the binary?

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

    ? ? ?? ? ?
  45. complex-ish

  46. None
  47. None
  48. Linking Basics Binary Framework

  49. Static Linking Binary Static Framework

  50. Dynamic Linking Binary Dynamic Framework

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

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

    ? ? ? ??? ? ? Dynamic Framework
  53. Libraries == More Complex recency bias

  54. Kermit Crashlytics Firebase.h Firebase

  55. Kermit Crashlytics Firebase.h Firebase Dynamic

  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
  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
  58. stay one lesson ahead…

  59. Kotlin/iOS Cocoapods *.h Binary

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

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

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

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

  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

  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

  66. Testing Binary Framework

  67. Kotlin/iOS test.kexe

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

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

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

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

  73. stay one lesson ahead…

  74. API Design thoughts on the common language

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

  76. Common Android iOS Firebase/JVM Firebase/iOS

  77. Common Android iOS Firebase/JVM Firebase/iOS

  78. Common Android iOS C code

  79. Common Android iOS C code JNI cinterop bridge

  80. Common Android iOS C code JNI cinterop bridge

  81. Rules are difficult “it depends” is everything

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

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

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

    }
  85. interface KotlinAnalytics { fun logEvent(name: String, parameters: Map<String, Any>) }

    expect fun platformAnalyticsFactory(): KotlinAnalytics
  86. None
  87. expect fun platformLogWriter(): LogWriter

  88. expect class CrashlyticsLogWriter( minSeverity: Severity = Severity.Info, minCrashSeverity: Severity =

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

    Severity.Warn, printTag: Boolean = true ) : LogWriter
  90. Service Objects/Small Graph interface and delegates work well

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

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

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

  94. actual typealias with great power…

  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 }
  96. JVM Side?

  97. import java.util.concurrent.atomic.AtomicInteger actual typealias AtomicInt = AtomicInteger

  98. Actual needs to match because obviously it does

  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) }
  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) }
  101. Platform Affinity android iOS ?

  102. Platform Affinity android iOS android-y

  103. typealias can be brittle

  104. typealias can be brittle

  105. Platform Affinity android iOS android-y JS

  106. Parallel Delegates android iOS android-y

  107. Empty Typealias everything is extensions

  108. expect class QuerySnapshot expect val QuerySnapshot.documentChanges_:List<DocumentChange> expect fun QuerySnapshot.getDocumentChanges_(…):List<DocumentChange> expect

    val QuerySnapshot.documents_:List<DocumentSnapshot> expect val QuerySnapshot.metadata: SnapshotMetadata expect val QuerySnapshot.query: Query expect val QuerySnapshot.empty: Boolean expect val QuerySnapshot.size: Int
  109. expect class QuerySnapshot expect val QuerySnapshot.documentChanges_:List<DocumentChange> expect fun QuerySnapshot.getDocumentChanges_(…):List<DocumentChange> expect

    val QuerySnapshot.documents_:List<DocumentSnapshot> expect val QuerySnapshot.metadata: SnapshotMetadata expect val QuerySnapshot.query: Query expect val QuerySnapshot.empty: Boolean expect val QuerySnapshot.size: Int
  110. actual typealias QuerySnapshot = FIRQuerySnapshot actual val QuerySnapshot.documentChanges_: List<DocumentChange> get()

    = documentChanges as List<DocumentChange> actual val QuerySnapshot.documents_: List<DocumentSnapshot> get() = documents as List<DocumentSnapshot> actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata documentChangesWithIncludeMetadataChanges(metadataChanges == Metad iOS
  111. actual typealias QuerySnapshot = FIRQuerySnapshot actual val QuerySnapshot.documentChanges_: List<DocumentChange> get()

    = documentChanges as List<DocumentChange> actual val QuerySnapshot.documents_: List<DocumentSnapshot> get() = documents as List<DocumentSnapshot> actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata documentChangesWithIncludeMetadataChanges(metadataChanges == Metad iOS
  112. actual typealias QuerySnapshot = FIRQuerySnapshot actual val QuerySnapshot.documentChanges_: List<DocumentChange> get()

    = documentChanges as List<DocumentChange> actual val QuerySnapshot.documents_: List<DocumentSnapshot> get() = documents as List<DocumentSnapshot> actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata documentChangesWithIncludeMetadataChanges(metadataChanges == Metad iOS
  113. expect open class DocumentSnapshot DocumentSnapshot actual typealias DocumentSnapshot = FIRDocumentSnapshot

    iOS common actual typealias DocumentSnapshot = com.google.firebase.firestore.DocumentSnapshot Android
  114. actual typealias QuerySnapshot = com.google.firebase.firestore.QuerySn actual val QuerySnapshot.documentChanges_: List<DocumentChange> get()

    = documentChanges actual val QuerySnapshot.documents_: List<DocumentSnapshot> get() = documents actual fun QuerySnapshot.getDocumentChanges_(metadataChanges: Metadata getDocumentChanges(metadataChanges.toJvm()) Android
  115. Platform Affinity

  116. Platform Agnostic

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

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

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

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

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

  123. Iterative Design tweak for platform specifics

  124. Platform-Specific Init then common

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

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

    Init Other Calls
  127. Simple First pass it in

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

  129. At Touchlab • Big team build tools • Better iOS

    Dev ex • Swift code generation • Other things
  130. If you’re facing these issues… @kpgalligan Kotlin Slack

  131. Thanks! @kpgalligan

  132. Thanks! @kpgalligan Join the team !