Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Multiplatform Libraries

Kotlin Multiplatform Libraries

Thoughts on building libraries for KMP.

58d1281770fe55a05a96600244ec8341?s=128

Kevin Galligan

November 06, 2019
Tweet

Transcript

  1. Kotlin Multiplatform Libraries Kevin Galligan

  2. Touchlab

  3. None
  4. Libraries are crucial

  5. None
  6. -Kevin Galligan “Shared UI is a history of pain and

    failure. Shared logic is the history of computers.”
  7. “The easiest overhead to predict with C++ is the need

    to build frameworks and libraries” -Eyal Guthmann (Dropbox blog post author)
  8. KMP is no exception in fact, more so

  9. KMP is lean

  10. You don’t get • Concurrency • Locale • Date/Time •

    File I/O • Networking • (most of JRE)
  11. Big hill to climb however…

  12. Kotlin ecosystem is strong lot’s of stuff out there

  13. decade of android libraries

  14. Very engaged community you’re here, right?

  15. Why consider KMP?

  16. lots of interest

  17. lots of interest

  18. Growth in deployment much more in 2020

  19. Android ecosystem is crowded hard to get focus

  20. Great time to get in much bigger impact

  21. What is Kotlin Multiplatform?

  22. Common JVM JS Native

  23. Common JVM JS Native iOS Mac Linux Windows Android/NDK Wasm

    Others… Java-6 Java-8 Android Browser Node
  24. Common JVM JS Native iOS Mac Linux Windows Android/NDK Wasm

    Others… Java-6 Java-8 Android Browser Node
  25. Common JVM JS Native iOS (arm 64) Java-6 Java-8 Android

    Browser Node
  26. Common Native iOS (arm 64) Xcode framework

  27. Common Native iOS (arm 64) klib Xcode framework

  28. Common JVM Android

  29. Common JVM Android App

  30. Common JVM Android App AAR

  31. Library Patterns

  32. Just Code not super complicated

  33. SDK Wrapper Firestore SDK

  34. Not super clear cut most libraries are a mix

  35. Published SDK many from one

  36. Platform Specific Logic

  37. expect/actual

  38. //In common code expect val isMainThread: Boolean

  39. //In common code expect val isMainThread: Boolean //In Android/JVM actual

    val isMainThread: Boolean get() = Looper.getMainLooper() === Looper.myLooper()
  40. //In common code expect val isMainThread: Boolean //In Android/JVM actual

    val isMainThread: Boolean get() = Looper.getMainLooper() === Looper.myLooper() //In iOS/native code actual val isMainThread: Boolean get() = NSThread.isMainThread()
  41. //Value expect val isMainThread: Boolean

  42. //Value expect val isMainThread: Boolean //Function expect fun myFun():String

  43. //Value expect val isMainThread: Boolean //Function expect fun myFun():String //Class

    expect class MyClass { fun heyo(): String }
  44. //Value expect val isMainThread: Boolean //Function expect fun myFun():String //Class

    expect class MyClass { fun heyo(): String } //Object expect object MyObject { fun heyo(): String }
  45. //Value expect val isMainThread: Boolean //Function expect fun myFun():String //Class

    expect class MyClass { fun heyo(): String } //Object expect object MyObject { fun heyo(): String } //Annotation @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR) @Retention(AnnotationRetention.SOURCE) expect annotation class Throws(vararg val exceptionClasses: KClass<out Throwable>)
  46. actual typealias with great power…

  47. /** * 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 }
  48. JVM Side?

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

  50. Actual needs to match because obviously it does

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

  54. Platform Affinity android iOS android-y

  55. Platform Affinity android iOS iOS-ish

  56. Platform Affinity android iOS iOS-ish JS

  57. Parallel Delegates android iOS android-y

  58. (Maybe) Optimize for JVM (if) most of your users

  59. Stately has a fair bit of stuff

  60. Minimal JVM-side impact

  61. typealias can be brittle

  62. Prefer Interfaces for “service” objects

  63. public interface Settings { public fun clear() public fun remove(key:

    String) public fun hasKey(key: String): Boolean public fun putInt(key: String, value: Int) public fun getInt(key: String, defaultValue: Int = 0): Int public fun getIntOrNull(key: String): Int? public fun putLong(key: String, value: Long) public fun getLong(key: String, defaultValue: Long = 0): Long public fun getLongOrNull(key: String): Long? //Etc... } from https://github.com/russhwolf/multiplatform-settings
  64. expect fun platformSettings():Settings

  65. object ServiceRegistry { var sessionizeApi:SessionizeApi by ThreadLocalDelegate() var analyticsApi: AnalyticsApi

    by FrozenDelegate() var notificationsApi:NotificationsApi by FrozenDelegate() var dbDriver: SqlDriver by FrozenDelegate() var cd: CoroutineDispatcher by FrozenDelegate() var appSettings: Settings by FrozenDelegate() var concurrent: Concurrent by FrozenDelegate() var timeZone: String by FrozenDelegate() //Etc… from https://github.com/touchlab/DroidconKotlin/
  66. Just an Interface no expect/actual required

  67. class TestSettings:Settings { private val map = frozenHashMap<String, Any?>() override

    fun clear() { map.clear() } override fun getBoolean(key: String, defaultValue: Boolean): Bo return if(map.containsKey(key)){ map[key] as Boolean }else{ defaultValue } } //Etc… from https://github.com/touchlab/DroidconKotlin/
  68. object ServiceRegistry { var sessionizeApi:SessionizeApi by ThreadLocalDelegate() var analyticsApi: AnalyticsApi

    by FrozenDelegate() var notificationsApi:NotificationsApi by FrozenDelegate() var dbDriver: SqlDriver by FrozenDelegate() var cd: CoroutineDispatcher by FrozenDelegate() var appSettings: Settings by FrozenDelegate() var concurrent: Concurrent by FrozenDelegate() var timeZone: String by FrozenDelegate() //Etc… from https://github.com/touchlab/DroidconKotlin/
  69. Minimize expect/actual Droidcon App: 9 functions, 2 classes

  70. https://go.touchlab.co/dcrw

  71. Firestore SDK wrapping multiple clients

  72. 2 (or more) Similar Things but not the same

  73. Platform Affinity android iOS ?

  74. Empty Typealias wait, hear me out

  75. None
  76. Platform Affinity

  77. Platform Agnostic

  78. Empty Typealias everything is extensions

  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. expect open class DocumentSnapshot DocumentSnapshot actual typealias DocumentSnapshot = FIRDocumentSnapshot

    iOS common actual typealias DocumentSnapshot = com.google.firebase.firestore.DocumentSnapshot Android
  85. 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
  86. Watch Return Types be careful of matching signatures

  87. expect fun FirebaseFirestore.disableNetwork_():TaskVoid

  88. expect fun FirebaseFirestore.disableNetwork_():TaskVoid actual fun FirebaseFirestore.disableNetwork_(): TaskVoid = TaskVoid(disableNetwork())

  89. expect fun FirebaseFirestore.disableNetwork_():TaskVoid actual fun FirebaseFirestore.disableNetwork_(): TaskVoid = TaskVoid(disableNetwork()) public

    Task<Void> disableNetwork() { this.ensureClientConfigured(); return this.client.disableNetwork(); }
  90. Again, Prefer Interfaces for service objects

  91. expect fun getFirebaseInstance():FirebaseFirestore expect class FirebaseFirestore expect fun FirebaseFirestore.batch(): WriteBatch

    expect fun FirebaseFirestore.collection(collectionPath:String):Collect expect fun FirebaseFirestore.collectionGroup(collectionId:String):Quer expect fun FirebaseFirestore.disableNetwork_():TaskVoid expect fun FirebaseFirestore.document(documentPath:String):DocumentRef expect fun FirebaseFirestore.enableNetwork_():TaskVoid expect var FirebaseFirestore.settings:FirebaseFirestoreSettings
  92. expect fun getFirebaseInstance():FirebaseFirestore interface FirebaseFirestore{ fun batch(): WriteBatch fun collection(collectionPath:String):CollectionReference

    fun collectionGroup(collectionId:String):Query fun disableNetwork():TaskVoid fun document(documentPath:String):DocumentReference fun enableNetwork():TaskVoid var FirebaseFirestore.settings:FirebaseFirestoreSettings }
  93. Minimize expect/actual? Firestore SDK: 100+ ‘expect’ instances

  94. Which to use? • Interfaces when reasonable • Singletons and

    service objects for sure • Easier to test • typealias when you need a bunch of things and don’t want a parallel delegate hierarchy • Data classes and type hierarchies get complicated with interfaces
  95. Inline Classes the other option

  96. inline class Name(val s: String) { val length: Int get()

    = s.length fun greet() { println("Hello, $s") } }
  97. inline class Name(val s: String) { val length: Int get()

    = s.length fun greet() { println("Hello, $s") } }
  98. Experimental had some trouble, but promising (1.3.70?)

  99. Function args! swift friendly

  100. fun initLambdas( staticFileLoader: (filePrefix: String, fileType: String) -> String?, clLogCallback:

    (s: String) -> Unit, softExceptionCallback: (e:Throwable, message:String) - >Unit)
  101. func loadAsset(filePrefix:String, fileType:String) -> String?{ do{ let bundleFile = Bundle.main.path(forResource:

    filePrefix, ofType: fileType) return try String(contentsOfFile: bundleFile!) } catch { return nil } }
  102. Hard(er) to test interfaces and functions are stubs

  103. Objc Platform Interop? sure, but meh

  104. configure([targets.iosX64, targets.iosArm64 ]) { compilations.main.source(sourceSets.iosMain) compilations.test.source(sourceSets.iosTest) compilations["main"].cinterops { firebasecore {

    packageName 'cocoapods.FirebaseCore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseCore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseCore-$ {versions.firebaseCoreIos}") } firestore { packageName 'cocoapods.FirebaseFirestore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseFirestore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseFirestore/Firestore/Source/Public", "$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseFirestore-$ {versions.firebaseFirestoreIos}") } } }
  105. configure([targets.iosX64, targets.iosArm64 ]) { compilations.main.source(sourceSets.iosMain) compilations.test.source(sourceSets.iosTest) compilations["main"].cinterops { firebasecore {

    packageName 'cocoapods.FirebaseCore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseCore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseCore-$ {versions.firebaseCoreIos}") } firestore { packageName 'cocoapods.FirebaseFirestore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseFirestore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseFirestore/Firestore/Source/Public", "$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseFirestore-$ {versions.firebaseFirestoreIos}") } } }
  106. configure([targets.iosX64, targets.iosArm64 ]) { compilations.main.source(sourceSets.iosMain) compilations.test.source(sourceSets.iosTest) compilations["main"].cinterops { firebasecore {

    packageName 'cocoapods.FirebaseCore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseCore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseCore-$ {versions.firebaseCoreIos}") } firestore { packageName 'cocoapods.FirebaseFirestore' defFile = file("$projectDir/src/iosMain/c_interop/FirebaseFirestore.def") includeDirs ("$projectDir/../iosApp/Pods/FirebaseFirestore/Firestore/Source/Public", "$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public") compilerOpts ("-F$projectDir/src/iosMain/c_interop/modules/FirebaseFirestore-$ {versions.firebaseFirestoreIos}") } } }
  107. @ExternalObjCClass open class FIRAppMeta : NSObjectMeta { val allApps: Map<Any?,

    *>? @ObjCMethod("allApps", "@16@0:8") external get @ObjCMethod("configure", "v16@0:8") external open fun configure(): Unit @ObjCMethod("configureWithOptions:", "v24@0:8@16") external open fun configureWithOptions(options: FIROptions): Unit @ObjCMethod("configureWithName:options:", "v32@0:8@16@24") external open fun configureWithName(name: String, options: FIROptions): Unit @ObjCMethod("defaultApp", "@16@0:8") external open fun defaultApp(): FIRApp? @ObjCMethod("appNamed:", "@24@0:8@16") external open fun appNamed(name: String): FIRApp? @ObjCMethod("allApps", "@16@0:8")
  108. Interop can be tricky with library brittle config (imho)

  109. Or…

  110. interface AnalyticsApi { fun logEvent(name: String, params: Map<String, Any>) }

  111. class FirebaseAnalyticsApi: AnalyticsApi{ func logEvent(name: String, params: [String : Any])

    { Analytics.logEvent(name, parameters: params) } }
  112. Firestore SDK needs objc maybe your library doesn’t

  113. Firestore SDK https://github.com/touchlab/FirestoreKMP

  114. Soft Launch: Crash Reporting

  115. Symbolicated Kotlin Crashes Crashlytics & Bugsnag

  116. The Problem different platforms

  117. Kotlin has Exceptions iOS mostly just crashes

  118. konan::abort()

  119. Throwable stack

  120. iOS Needs Interop Crashlytics & Bugsnag libraries

  121. class CrashNSException: NSException { init(callStack:[NSNumber], exceptionType: String, message: String) {

    super.init(name: NSExceptionName(rawValue: exceptionType), reason: message, userInfo: nil) self._callStackReturnAddresses = callStack } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private lazy var _callStackReturnAddresses: [NSNumber] = [] override var callStackReturnAddresses: [NSNumber] { get { return _callStackReturnAddresses } set { _callStackReturnAddresses = newValue } } } class BugsnagCrashHandler: CrashkiosCrashHandler { override func crashParts(addresses: [KotlinLong], exceptionType: String, message: String) { Bugsnag.notify(CrashNSException(callStack: addresses, exceptionType: exceptionType, message: message)) } }
  122. class CrashlyticsCrashHandler: CrashkiosCrashHandler { override func crashParts( addresses: [KotlinLong], exceptionType:

    String, message: String) { let clsStackTrace = addresses.map { CLSStackFrame(address: UInt(truncating: $0)) } Crashlytics.sharedInstance().recordCustomExceptionName( exceptionType, reason: message, frameArray: clsStackTrace ) } }
  123. CrashKiOS https://github.com/touchlab/CrashKiOS

  124. CrashKiOS https://github.com/touchlab/CrashKiOS

  125. Publishing/CI

  126. Publishing • Maven Central • Bintray (jcenter) • Jitpack does

    not work
  127. Maven Central • https://central.sonatype.org/pages/ossrh-guide.html • https://github.com/cashapp/sqldelight • https://github.com/touchlab/FirestoreKMP

  128. Bintray natanfudge.github.io/fudgedocs/publish-kotlin-mpp-lib.html

  129. CI • Travis by a lot of the Square stuff

    • We’ve had some luck with “App Center” • Moving to Azure Pipelines as it makes Windows easier
  130. Getting Started go.touchlab.co/kmplib

  131. take a break!

  132. General Thoughts

  133. Big Blockers stuff we’re waiting on

  134. Compiler Plugins no kapt

  135. Multithreaded Coroutines cascading issue

  136. None
  137. None
  138. What does that mean?

  139. go.touchlab.co/dcktsrc

  140. None
  141. Institutional Libraries the usual stuff

  142. Institutional Libraries •Date/Time •Locale •Files •I/O

  143. date libraries

  144. date libraries

  145. date libraries

  146. date libraries

  147. date libraries

  148. What to work on? be a little careful

  149. Skip date/time libraries that’s covered

  150. Mocking no reflection or compiler plugins

  151. Mockk wants help

  152. What about Swift? no reflection, no surrender

  153. interface HeyStuff { fun myFun(): String val myVal: Int }

  154. interface HeyStuff { fun myFun(): String val myVal: Int }

    class MockStuff:HeyStuff{ }
  155. interface HeyStuff { fun myFun(): String val myVal: Int }

    class MockStuff:HeyStuff{ }
  156. File I/O probably OK

  157. Server Contract swagger, graphql, grpc

  158. Architecture big word

  159. Reactive Border LiveData analog? RxSwift?

  160. Logging waiting on timber

  161. UI? wait, I thought…

  162. Join the Slack let’s work together

  163. Avoid Library Bikeshedding!

  164. Touchlab Stuff

  165. Some Stuff • Kotlin Xcode Plugin • Xcode Sync (get

    you Kotlin in Xcode) • Stately • Sqliter (Sqlite Driver under Sqldelight on native)
  166. KMP evaluation kit

  167. KaMP-Kit go.touchlab.co/KaMP-Kit

  168. Thanks! @kpgalligan touchlab.co

  169. Thanks! @kpgalligan touchlab.co Join the team !