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

Kotlin Multiplatform Libraries

Kotlin Multiplatform Libraries

Thoughts on building libraries for KMP.

Avatar for Kevin Galligan

Kevin Galligan

November 06, 2019
Tweet

More Decks by Kevin Galligan

Other Decks in Programming

Transcript

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

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

    to build frameworks and libraries” -Eyal Guthmann (Dropbox blog post author)
  3. You don’t get • Concurrency • Locale • Date/Time •

    File I/O • Networking • (most of JRE)
  4. Common JVM JS Native iOS Mac Linux Windows Android/NDK Wasm

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

    Others… Java-6 Java-8 Android Browser Node
  6. //In common code expect val isMainThread: Boolean //In Android/JVM actual

    val isMainThread: Boolean get() = Looper.getMainLooper() === Looper.myLooper()
  7. //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()
  8. //Value expect val isMainThread: Boolean //Function expect fun myFun():String //Class

    expect class MyClass { fun heyo(): String } //Object expect object MyObject { fun heyo(): String }
  9. //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>)
  10. /** * 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 }
  11. 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) }
  12. 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) }
  13. 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
  14. 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/
  15. 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/
  16. 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/
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. expect open class DocumentSnapshot DocumentSnapshot actual typealias DocumentSnapshot = FIRDocumentSnapshot

    iOS common actual typealias DocumentSnapshot = com.google.firebase.firestore.DocumentSnapshot Android
  23. 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
  24. expect fun FirebaseFirestore.disableNetwork_():TaskVoid actual fun FirebaseFirestore.disableNetwork_(): TaskVoid = TaskVoid(disableNetwork()) public

    Task<Void> disableNetwork() { this.ensureClientConfigured(); return this.client.disableNetwork(); }
  25. 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
  26. 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 }
  27. 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
  28. inline class Name(val s: String) { val length: Int get()

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

    = s.length fun greet() { println("Hello, $s") } }
  30. fun initLambdas( staticFileLoader: (filePrefix: String, fileType: String) -> String?, clLogCallback:

    (s: String) -> Unit, softExceptionCallback: (e:Throwable, message:String) - >Unit)
  31. 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 } }
  32. 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}") } } }
  33. 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}") } } }
  34. 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}") } } }
  35. @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")
  36. 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)) } }
  37. 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 ) } }
  38. 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
  39. Some Stuff • Kotlin Xcode Plugin • Xcode Sync (get

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