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

Bottom-Up Code-Sharing with Kotlin Multiplatform

Bottom-Up Code-Sharing with Kotlin Multiplatform

Kotlin Multiplatform enables sharing logic and architecture across platforms while still interacting with each platform’s native APIs. We’ll talk through what this looks like by walking through a sample application. We’ll highlight helpful patterns for both production and test code, as well as available tools and dependencies. Once we’re done you’ll be ready to leverage Kotlin’s code-sharing capabilities in your own projects, and never need to write the same logic twice ever again.

Russell Wolf

August 14, 2019
Tweet

More Decks by Russell Wolf

Other Decks in Technology

Transcript

  1. About Me • @RussHWolf ( or ) • Android Developer

    at Rocket Insights • Organizer of Kotlin Office Hours in Boston • Author of Multiplatform Settings
  2. Multiplatform Kotlin • Compile common code to multiple targets •

    JVM, JS, Android, Desktop, iOS, Embedded, WASM • Use platform-specific code to access platform APIs
  3. Kotlin/Native • Compile Kotlin to LLVM targets • No VM!

    • Desktop, Embedded, Mobile • Interop with C or Objective-C
  4. Mobile Multiplatform • Android (JVM) / iOS (Native) • Mobile

    is the killer app for Multiplatform • Same use-case • Similar capabilities
  5. A Note on Colors • This presentation is color-coded by

    platform
 
 Common
 Android
 iOS
 JVM
 Gradle (KTS)
 Swift/SQL/Other
  6. Platform-Specific Code interface Foo { ... } class AndroidFoo :

    Foo { ... } class IosFoo : Foo { ... } class SwiftFoo : NsObject, Foo { ... } class MockFoo : Foo { ... }
  7. Soluna • https://github.com/russhwolf/ soluna • Compute rise/set times of Sun

    and Moon • Use-cases • Calendar • Mobile Notifications • API? • WIP
  8. Core • Rise/Set calculation • Compute position of sun/moon at

    noon in equatorial coordinates • Solve for time when that position is on horizon • DegreeMath • No dates or time zones • TODO - Moon calculation is buggy
  9. Core fun sunTimes( year: Int, month: Int, day: Int, offset:

    Double, // Hours latitude: Double, // Degrees longitude: Double // Degrees ): Pair<Double?, Double?> { // Hours ... return riseTime?.value to setTime?.value }
  10. Core interface AngleUnit<T : AngleUnit<T>> { val value: Double }

    inline class Degree( override val value: Double ) : AngleUnit<Degree> fun sin(degrees: Degree) = kotlin.math.sin(degrees.toRadians().value)
  11. Calendar • java.awt drawing commands • java.time date manipulation •

    TODO • Architecture/Tests • CLI • TornadoFx UI
  12. Calendar for (i in 1..numberOfDays) { val date = LocalDate.of(year,

    month, i) val (sunRiseTime, sunSetTime) = sunTimes(year, month.value, i, 0.0, latitude, longitude) val sunRiseDate: ZonedDateTime = sunRiseTime?.toDateTime(date, timeZone) val sunSetDate: ZonedDateTime = sunSetTime?.toDateTime(date, timeZone) ... }
  13. Mobile • Configure alert notifications based on rise/set times •

    Store configuration in SqlDelight database • Manual coords or geocode via Google Maps API • Schedule and generate alerts using native APIs • Still in progress...
  14. Data Models • Location • Label, Latitude, Longitude, Time Zone

    • Reminder • Type (rise/set), Minutes Before • Many-to-One relationship
  15. Mobile Architecture • Database stores locations/ reminders • Google Maps

    API helps populate location data • Repository & ViewModels • Coroutine architecture Google Api Client Repository ViewModel ViewModel ViewModel Database
  16. SqlDelight CREATE TABLE location ( id INTEGER NOT NULL PRIMARY

    KEY AUTOINCREMENT, label TEXT NOT NULL UNIQUE, latitude REAL NOT NULL, longitude REAL NOT NULL, timeZone TEXT NOT NULL ); selectAllLocations: SELECT id, label FROM location; 

  17. SqlDelight data class Location( val id: Long, val label: String,

    val latitude: Double, val longitude: Double, val timeZone: String )
 
 override fun selectAllLocations(): 
 Query<SelectAllLocations> = ...
  18. Ktor val httpClient = HttpClient(httpClientEngine) { defaultRequest { url.protocol =

    URLProtocol.HTTPS url.host = "maps.googleapis.com/maps/api" parameter("key", BuildKonfig.GOOGLE_API_KEY) } install(JsonFeature) { serializer = KotlinxSerializer().apply { setMapper(
 GeocodeResponse::class, 
 GeocodeResponse.serializer()) } } }
  19. Ktor suspend fun getGeocode(placeId: String):
 GeocodeResponse = httpClient.get { url

    { encodedPath = "geocode/json" parameter("place_id", placeId) } }
  20. Repository • Central data layer • Wraps Sql calls in

    suspend functions • Coordinates API calls
  21. Native Threading is Hard • Data must be frozen to

    pass between threads • myData.freeze() • Different paradigm between Native and JVM
  22. Background Tasks expect suspend fun <T> runInBackground(block: () -> T):

    T
 
 actual suspend fun <T> runInBackground(block: () -> T): T =
 withContext(Dispatchers.IO) { block() }
 actual suspend fun <T> runInBackground(block: () -> T): T = 
 suspendCoroutine<T> { continuation -> val future = worker.execute(TransferMode.SAFE, 
 { block.freeze() }, 
 { it() }
 ) future.consume { continuation.resume(it) } }
  23. Repository suspend fun geocodeLocation(location: String): 
 GeocodeData? {
 val placeId

    = 
 googleApiClient.getPlaceAutocomplete(location) val coords = 
 googleApiClient.getGeocode(placeId)
 val timeZone = 
 googleApiClient
 .getTimeZone(coords.lat, coords.lng, epochSeconds)
 return GeocodeData(coords.lat, coords.lng, timeZone) }
  24. ViewModels var state: T by Delegates.observable() { 
 _, _,

    newValue -> viewStateListener?.invoke(newValue) }
 
 protected fun updateAsync(
 action: suspend (T) -> T
 ): Job = coroutineScope.launch { state = action(state) }
  25. ViewModels fun geocodeLocation(location: String) = 
 updateAsync { val geocodeData

    = 
 repository.geocodeLocation(location) state.copy(
 geocodeTrigger =
 EventTrigger.create(geocodeData)
 ) }
  26. iOS App • Swift/Xcode • Consumes ViewModels and renders UI

    • xcode-compat Gradle plugin • https://github.com/Kotlin/ xcode-compat
  27. iOS App val repository: SelunaRepository = ...
 
 let viewModel

    = AddLocationViewModel(
 repository: SwiftKotlinBridgeKt.repository
 )
 
 @IBAction func onGeocodeClick(_ sender: Any) { viewModel.geocodeLocation(
 location: labelInput.text ?? ""
 ) }
  28. iOS Tests tasks.create("iosTest") { dependsOn("linkDebugTestIos") doLast { val testBinaryPath =

    (kotlin.targets["ios"] as KotlinNativeTarget)
 .binaries
 .getTest("DEBUG")
 .outputFile.absolutePath exec { commandLine(
 "xcrun", "simctl", "spawn", "iPhone Xʀ", testBinaryPath
 ) } } }
  29. Other Notes • BuildKonfig
 https://github.com/yshrsmz/BuildKonfig
 
 buildConfigField(
 FieldSpec.Type.STRING, 
 "GOOGLE_API_KEY"


    "...") • Obj-C/Swift Generics 
 https://kotlinlang.org/docs/reference/native/objc_interop.html
 
 (targets["ios"] as KotlinNativeTarget)
 .compilations["main"]
 .extraOpts
 .add("-Xobjc-generics")
  30. Other Notes • Native Debuggers • AppCode, IDEA Ultimate EAP

    • Xcode (Touchlab) • Testing is hard • No multiplatform mocking
  31. TODO • Integrate reminders in data layer • Lots more

    UI • Android! • Notifications • (and actually call into Core)
  32. Lessons • Libraries! SqlDelight, Coroutines, Ktor, Serialization, BuildKonfig • Layered

    architecture to separate concerns • Share what's worth sharing
  33. Thanks! • Questions? • @RussHWolf ( or ) • Project:

    https://github.com/russhwolf/soluna • Gradle Docs: https://kotlinlang.org/docs/reference/building- mpp-with-gradle.html • Library resources: • https://github.com/AAkira/Kotlin-Multiplatform-Libraries • https://github.com/bipinvaylu/awesome-kotlin- multiplatform • https://github.com/topics/kotlin-multiplatform