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

Kotlin Multiplatform Libraries

Kotlin Multiplatform Libraries

Thoughts on building libraries for KMP.

Kevin Galligan

November 06, 2019
Tweet

More Decks by Kevin Galligan

Other Decks in Programming

Transcript

  1. Kotlin Multiplatform Libraries
    Kevin Galligan

    View Slide

  2. Touchlab

    View Slide

  3. View Slide

  4. Libraries are crucial

    View Slide

  5. View Slide

  6. -Kevin Galligan
    “Shared UI is a history of pain and failure. Shared
    logic is the history of computers.”

    View Slide

  7. “The easiest overhead to predict with C++ is
    the need to build frameworks and libraries”
    -Eyal Guthmann (Dropbox blog post author)

    View Slide

  8. KMP is no exception
    in fact, more so

    View Slide

  9. KMP is lean

    View Slide

  10. You don’t get
    • Concurrency
    • Locale
    • Date/Time
    • File I/O
    • Networking
    • (most of JRE)

    View Slide

  11. Big hill to climb
    however…

    View Slide

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

    View Slide

  13. decade of android libraries

    View Slide

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

    View Slide

  15. Why consider KMP?

    View Slide

  16. lots of interest

    View Slide

  17. lots of interest

    View Slide

  18. Growth in deployment
    much more in 2020

    View Slide

  19. Android ecosystem is crowded
    hard to get focus

    View Slide

  20. Great time to get in
    much bigger impact

    View Slide

  21. What is Kotlin Multiplatform?

    View Slide

  22. Common
    JVM JS Native

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  26. Common
    Native
    iOS (arm 64)
    Xcode
    framework

    View Slide

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

    View Slide

  28. Common
    JVM
    Android

    View Slide

  29. Common
    JVM
    Android
    App

    View Slide

  30. Common
    JVM
    Android
    App AAR

    View Slide

  31. Library Patterns

    View Slide

  32. Just Code
    not super complicated

    View Slide

  33. SDK Wrapper
    Firestore SDK

    View Slide

  34. Not super clear cut
    most libraries are a mix

    View Slide

  35. Published SDK
    many from one

    View Slide

  36. Platform Specific Logic

    View Slide

  37. expect/actual

    View Slide

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

    View Slide

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

    View Slide

  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()

    View Slide

  41. //Value
    expect val isMainThread: Boolean

    View Slide

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

    View Slide

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

    View Slide

  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
    }

    View Slide

  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: KClassThrowable>)

    View Slide

  46. actual typealias
    with great power…

    View Slide

  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
    }

    View Slide

  48. JVM Side?

    View Slide

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

    View Slide

  50. Actual needs to match
    because obviously it does

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  53. Platform Affinity
    android iOS
    ?

    View Slide

  54. Platform Affinity
    android iOS
    android-y

    View Slide

  55. Platform Affinity
    android iOS
    iOS-ish

    View Slide

  56. Platform Affinity
    android iOS
    iOS-ish
    JS

    View Slide

  57. Parallel Delegates
    android iOS
    android-y

    View Slide

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

    View Slide

  59. Stately has a fair bit of stuff

    View Slide

  60. Minimal JVM-side impact

    View Slide

  61. typealias can be brittle

    View Slide

  62. Prefer Interfaces
    for “service” objects

    View Slide

  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

    View Slide

  64. expect fun platformSettings():Settings

    View Slide

  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/

    View Slide

  66. Just an Interface
    no expect/actual required

    View Slide

  67. class TestSettings:Settings {
    private val map = frozenHashMap()
    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/

    View Slide

  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/

    View Slide

  69. Minimize expect/actual
    Droidcon App: 9 functions, 2 classes

    View Slide

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

    View Slide

  71. Firestore SDK
    wrapping multiple clients

    View Slide

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

    View Slide

  73. Platform Affinity
    android iOS
    ?

    View Slide

  74. Empty Typealias
    wait, hear me out

    View Slide

  75. View Slide

  76. Platform Affinity

    View Slide

  77. Platform Agnostic

    View Slide

  78. Empty Typealias
    everything is extensions

    View Slide

  79. 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

  80. 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

  81. 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

  82. 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

  83. 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

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

    View Slide

  85. 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

  86. Watch Return Types
    be careful of matching signatures

    View Slide

  87. expect fun FirebaseFirestore.disableNetwork_():TaskVoid

    View Slide

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

    View Slide

  89. expect fun FirebaseFirestore.disableNetwork_():TaskVoid
    actual fun FirebaseFirestore.disableNetwork_(): TaskVoid =
    TaskVoid(disableNetwork())
    public Task disableNetwork() {
    this.ensureClientConfigured();
    return this.client.disableNetwork();
    }

    View Slide

  90. Again, Prefer Interfaces
    for service objects

    View Slide

  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

    View Slide

  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
    }

    View Slide

  93. Minimize expect/actual?
    Firestore SDK: 100+ ‘expect’ instances

    View Slide

  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

    View Slide

  95. Inline Classes
    the other option

    View Slide

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

    View Slide

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

    View Slide

  98. Experimental
    had some trouble, but promising (1.3.70?)

    View Slide

  99. Function args!
    swift friendly

    View Slide

  100. fun initLambdas(
    staticFileLoader: (filePrefix: String, fileType: String)
    -> String?,
    clLogCallback: (s: String) -> Unit,
    softExceptionCallback: (e:Throwable, message:String) -
    >Unit)

    View Slide

  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
    }
    }

    View Slide

  102. Hard(er) to test
    interfaces and functions are stubs

    View Slide

  103. Objc Platform Interop?
    sure, but meh

    View Slide

  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}")
    }
    }
    }

    View Slide

  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}")
    }
    }
    }

    View Slide

  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}")
    }
    }
    }

    View Slide

  107. @ExternalObjCClass open class FIRAppMeta : NSObjectMeta {
    val allApps: Map?
    @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")

    View Slide

  108. Interop can be tricky with library
    brittle config (imho)

    View Slide

  109. Or…

    View Slide

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

    View Slide

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

    View Slide

  112. Firestore SDK needs objc
    maybe your library doesn’t

    View Slide

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

    View Slide

  114. Soft Launch: Crash Reporting

    View Slide

  115. Symbolicated Kotlin Crashes
    Crashlytics & Bugsnag

    View Slide

  116. The Problem
    different platforms

    View Slide

  117. Kotlin has Exceptions
    iOS mostly just crashes

    View Slide

  118. konan::abort()

    View Slide

  119. Throwable stack

    View Slide

  120. iOS Needs Interop
    Crashlytics & Bugsnag libraries

    View Slide

  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))
    }
    }

    View Slide

  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
    )
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  125. Publishing/CI

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  130. Getting Started
    go.touchlab.co/kmplib

    View Slide

  131. take a break!

    View Slide

  132. General Thoughts

    View Slide

  133. Big Blockers
    stuff we’re waiting on

    View Slide

  134. Compiler Plugins
    no kapt

    View Slide

  135. Multithreaded Coroutines
    cascading issue

    View Slide

  136. View Slide

  137. View Slide

  138. What does that mean?

    View Slide

  139. go.touchlab.co/dcktsrc

    View Slide

  140. View Slide

  141. Institutional Libraries
    the usual stuff

    View Slide

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

    View Slide

  143. date libraries

    View Slide

  144. date libraries

    View Slide

  145. date libraries

    View Slide

  146. date libraries

    View Slide

  147. date libraries

    View Slide

  148. What to work on?
    be a little careful

    View Slide

  149. Skip date/time libraries
    that’s covered

    View Slide

  150. Mocking
    no reflection or compiler plugins

    View Slide

  151. Mockk wants help

    View Slide

  152. What about Swift?
    no reflection, no surrender

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  156. File I/O
    probably OK

    View Slide

  157. Server Contract
    swagger, graphql, grpc

    View Slide

  158. Architecture
    big word

    View Slide

  159. Reactive Border
    LiveData analog?
    RxSwift?

    View Slide

  160. Logging
    waiting on timber

    View Slide

  161. UI?
    wait, I thought…

    View Slide

  162. Join the Slack
    let’s work together

    View Slide

  163. Avoid Library Bikeshedding!

    View Slide

  164. Touchlab Stuff

    View Slide

  165. Some Stuff
    • Kotlin Xcode Plugin
    • Xcode Sync (get you Kotlin in Xcode)
    • Stately
    • Sqliter (Sqlite Driver under Sqldelight on native)

    View Slide

  166. KMP evaluation kit

    View Slide

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

    View Slide

  168. Thanks!
    @kpgalligan
    touchlab.co

    View Slide

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

    View Slide