Kotlin Multiplatform Libraries

Kotlin Multiplatform Libraries

Thoughts on building libraries for KMP.

58d1281770fe55a05a96600244ec8341?s=128

Kevin Galligan

November 06, 2019
Tweet

Transcript

  1. 3.
  2. 5.
  3. 6.

    -Kevin Galligan “Shared UI is a history of pain and

    failure. Shared logic is the history of computers.”
  4. 7.

    “The easiest overhead to predict with C++ is the need

    to build frameworks and libraries” -Eyal Guthmann (Dropbox blog post author)
  5. 10.

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

    File I/O • Networking • (most of JRE)
  6. 23.

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

    Others… Java-6 Java-8 Android Browser Node
  7. 24.

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

    Others… Java-6 Java-8 Android Browser Node
  8. 39.

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

    val isMainThread: Boolean get() = Looper.getMainLooper() === Looper.myLooper()
  9. 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()
  10. 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 }
  11. 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>)
  12. 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 }
  13. 48.
  14. 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) }
  15. 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) }
  16. 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
  17. 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/
  18. 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/
  19. 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/
  20. 75.
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 84.

    expect open class DocumentSnapshot DocumentSnapshot actual typealias DocumentSnapshot = FIRDocumentSnapshot

    iOS common actual typealias DocumentSnapshot = com.google.firebase.firestore.DocumentSnapshot Android
  27. 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
  28. 89.

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

    Task<Void> disableNetwork() { this.ensureClientConfigured(); return this.client.disableNetwork(); }
  29. 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
  30. 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 }
  31. 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
  32. 96.

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

    = s.length fun greet() { println("Hello, $s") } }
  33. 97.

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

    = s.length fun greet() { println("Hello, $s") } }
  34. 100.

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

    (s: String) -> Unit, softExceptionCallback: (e:Throwable, message:String) - >Unit)
  35. 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 } }
  36. 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}") } } }
  37. 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}") } } }
  38. 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}") } } }
  39. 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")
  40. 109.
  41. 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)) } }
  42. 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 ) } }
  43. 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
  44. 136.
  45. 137.
  46. 140.
  47. 165.

    Some Stuff • Kotlin Xcode Plugin • Xcode Sync (get

    you Kotlin in Xcode) • Stately • Sqliter (Sqlite Driver under Sqldelight on native)