Slide 1

Slide 1 text

Kotlin Multiplatform Libraries Kevin Galligan

Slide 2

Slide 2 text

Touchlab

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Libraries are crucial

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

KMP is no exception in fact, more so

Slide 9

Slide 9 text

KMP is lean

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Big hill to climb however…

Slide 12

Slide 12 text

Kotlin ecosystem is strong lot’s of stuff out there

Slide 13

Slide 13 text

decade of android libraries

Slide 14

Slide 14 text

Very engaged community you’re here, right?

Slide 15

Slide 15 text

Why consider KMP?

Slide 16

Slide 16 text

lots of interest

Slide 17

Slide 17 text

lots of interest

Slide 18

Slide 18 text

Growth in deployment much more in 2020

Slide 19

Slide 19 text

Android ecosystem is crowded hard to get focus

Slide 20

Slide 20 text

Great time to get in much bigger impact

Slide 21

Slide 21 text

What is Kotlin Multiplatform?

Slide 22

Slide 22 text

Common JVM JS Native

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Common Native iOS (arm 64) Xcode framework

Slide 27

Slide 27 text

Common Native iOS (arm 64) klib Xcode framework

Slide 28

Slide 28 text

Common JVM Android

Slide 29

Slide 29 text

Common JVM Android App

Slide 30

Slide 30 text

Common JVM Android App AAR

Slide 31

Slide 31 text

Library Patterns

Slide 32

Slide 32 text

Just Code not super complicated

Slide 33

Slide 33 text

SDK Wrapper Firestore SDK

Slide 34

Slide 34 text

Not super clear cut most libraries are a mix

Slide 35

Slide 35 text

Published SDK many from one

Slide 36

Slide 36 text

Platform Specific Logic

Slide 37

Slide 37 text

expect/actual

Slide 38

Slide 38 text

//In common code expect val isMainThread: Boolean

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

//Value expect val isMainThread: Boolean

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

actual typealias with great power…

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

JVM Side?

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Actual needs to match because obviously it does

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Platform Affinity android iOS ?

Slide 54

Slide 54 text

Platform Affinity android iOS android-y

Slide 55

Slide 55 text

Platform Affinity android iOS iOS-ish

Slide 56

Slide 56 text

Platform Affinity android iOS iOS-ish JS

Slide 57

Slide 57 text

Parallel Delegates android iOS android-y

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Stately has a fair bit of stuff

Slide 60

Slide 60 text

Minimal JVM-side impact

Slide 61

Slide 61 text

typealias can be brittle

Slide 62

Slide 62 text

Prefer Interfaces for “service” objects

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

expect fun platformSettings():Settings

Slide 65

Slide 65 text

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/

Slide 66

Slide 66 text

Just an Interface no expect/actual required

Slide 67

Slide 67 text

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/

Slide 68

Slide 68 text

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/

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

https://go.touchlab.co/dcrw

Slide 71

Slide 71 text

Firestore SDK wrapping multiple clients

Slide 72

Slide 72 text

2 (or more) Similar Things but not the same

Slide 73

Slide 73 text

Platform Affinity android iOS ?

Slide 74

Slide 74 text

Empty Typealias wait, hear me out

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

Platform Affinity

Slide 77

Slide 77 text

Platform Agnostic

Slide 78

Slide 78 text

Empty Typealias everything is extensions

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Watch Return Types be careful of matching signatures

Slide 87

Slide 87 text

expect fun FirebaseFirestore.disableNetwork_():TaskVoid

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

Again, Prefer Interfaces for service objects

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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 }

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Inline Classes the other option

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

Experimental had some trouble, but promising (1.3.70?)

Slide 99

Slide 99 text

Function args! swift friendly

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Hard(er) to test interfaces and functions are stubs

Slide 103

Slide 103 text

Objc Platform Interop? sure, but meh

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

Interop can be tricky with library brittle config (imho)

Slide 109

Slide 109 text

Or…

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

Firestore SDK needs objc maybe your library doesn’t

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

Soft Launch: Crash Reporting

Slide 115

Slide 115 text

Symbolicated Kotlin Crashes Crashlytics & Bugsnag

Slide 116

Slide 116 text

The Problem different platforms

Slide 117

Slide 117 text

Kotlin has Exceptions iOS mostly just crashes

Slide 118

Slide 118 text

konan::abort()

Slide 119

Slide 119 text

Throwable stack

Slide 120

Slide 120 text

iOS Needs Interop Crashlytics & Bugsnag libraries

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

Publishing/CI

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

Getting Started go.touchlab.co/kmplib

Slide 131

Slide 131 text

take a break!

Slide 132

Slide 132 text

General Thoughts

Slide 133

Slide 133 text

Big Blockers stuff we’re waiting on

Slide 134

Slide 134 text

Compiler Plugins no kapt

Slide 135

Slide 135 text

Multithreaded Coroutines cascading issue

Slide 136

Slide 136 text

No content

Slide 137

Slide 137 text

No content

Slide 138

Slide 138 text

What does that mean?

Slide 139

Slide 139 text

go.touchlab.co/dcktsrc

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

Institutional Libraries the usual stuff

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

date libraries

Slide 144

Slide 144 text

date libraries

Slide 145

Slide 145 text

date libraries

Slide 146

Slide 146 text

date libraries

Slide 147

Slide 147 text

date libraries

Slide 148

Slide 148 text

What to work on? be a little careful

Slide 149

Slide 149 text

Skip date/time libraries that’s covered

Slide 150

Slide 150 text

Mocking no reflection or compiler plugins

Slide 151

Slide 151 text

Mockk wants help

Slide 152

Slide 152 text

What about Swift? no reflection, no surrender

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

File I/O probably OK

Slide 157

Slide 157 text

Server Contract swagger, graphql, grpc

Slide 158

Slide 158 text

Architecture big word

Slide 159

Slide 159 text

Reactive Border LiveData analog? RxSwift?

Slide 160

Slide 160 text

Logging waiting on timber

Slide 161

Slide 161 text

UI? wait, I thought…

Slide 162

Slide 162 text

Join the Slack let’s work together

Slide 163

Slide 163 text

Avoid Library Bikeshedding!

Slide 164

Slide 164 text

Touchlab Stuff

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

KMP evaluation kit

Slide 167

Slide 167 text

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

Slide 168

Slide 168 text

Thanks! @kpgalligan touchlab.co

Slide 169

Slide 169 text

Thanks! @kpgalligan touchlab.co Join the team !