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

SDK Design and Publishing For Kotlin Multiplatform Mobile

SDK Design and Publishing For Kotlin Multiplatform Mobile

Most Android developers who are aware of KMP know that you can code "once" then publish to iOS. That is technically true. However, as you progress beyond "POC" to integrating and shipping real production code, the design of the "SDK surface" becomes more complicated and nuanced (or, possibly, problematic). Many SDK design considerations are common, whether you have a Kotlin module in a single monorepo, are an internal team publishing SDK's for your org (a very common KMP use case), or a vendor publishing an SDK for public consumption. Some topics we'll cover: An Android consumer can see all the richness of the Kotlin language, but the iOS side gets filtered through Objctive-C into Swift. Much can get lost along the way. You need to understand and plan for this. Exposing too much to the iOS surface not only makes navigating documentation difficult but can have a significant binary size impact. We'll discuss what to look for and the techniques to use. Packaging and publishing of iOS artifacts have a number of things to consider (there's a bit on that). We'll walk through these topics and more to share what we've learned over the last few years of KMP/KMM production development.

Kevin Galligan

July 06, 2022
Tweet

More Decks by Kevin Galligan

Other Decks in Technology

Transcript

  1. SDK Design and Publishing For Kotlin
    Multiplatform Mobile
    Kevin Galligan

    View full-size slide

  2. Partner at
    Kotlin GDE

    View full-size slide

  3. Lots of Promises
    final of talk arc

    View full-size slide

  4. Android Conference
    just native mobile

    View full-size slide

  5. Kotlin (common)
    Android Framework
    Android


    Stuff
    iOS


    Stuff

    View full-size slide

  6. Android side?
    just kotlin

    View full-size slide

  7. iOS side?
    well…

    View full-size slide

  8. Kotlin —> Xcode Framework
    nice, done!

    View full-size slide

  9. API Not Ideal
    you can definitely make it worse

    View full-size slide

  10. The Big Issues
    or this talk’s TOC

    View full-size slide

  11. Kotlin through an Objc lens
    lowest common denominator

    View full-size slide

  12. Modularization
    one framework, name collisions, …

    View full-size slide

  13. Limit Public API
    nothing extra

    View full-size slide

  14. Packaging and Team Stuff
    some thoughts about practical approaches

    View full-size slide

  15. “Everything Else”
    just shove it in there

    View full-size slide

  16. Writing code 2x is insane!
    if you didn’t need to, of course

    View full-size slide

  17. “Kotlin doesn’t do ___”
    misses the point

    View full-size slide

  18. Yes, Currently 2x is Best
    in your org, with your team, hired/trained for 2x

    View full-size slide

  19. Native isn’t going away
    but there is consolidation

    View full-size slide

  20. UX Complexity/Sensitivity
    2014 Not-Native
    Native

    View full-size slide

  21. No Precise Data
    obviously

    View full-size slide

  22. UX Complexity/Sensitivity
    2018 Not-Native
    Native

    View full-size slide

  23. UX Complexity/Sensitivity
    2022 Not-Native
    Native

    View full-size slide

  24. UX Complexity/Sensitivity
    The boring middle Not-Native
    Native

    View full-size slide

  25. UX Complexity/Sensitivity
    2026? Not-Native
    Native

    View full-size slide

  26. KMP UI Story
    it’s coming

    View full-size slide

  27. compose UI on iOS

    View full-size slide

  28. UX Complexity/Sensitivity
    The boring middle
    +
    Not-Native
    Native

    View full-size slide

  29. iOS Compose will be OK
    probably not perfect

    View full-size slide

  30. Optionally Native
    much lower platform risk

    View full-size slide

  31. But I digress…

    View full-size slide

  32. Shared Code Overhead
    this is the issue

    View full-size slide

  33. Zero Overhead?
    we wouldn’t write 2x

    View full-size slide

  34. All Options !=
    kotlin focused on interop

    View full-size slide

  35. If *I* was on your team
    kmp party!

    View full-size slide

  36. That’s what we’re here to talk about…
    bringing us back from the technical odyssey

    View full-size slide

  37. notes on the android side

    View full-size slide

  38. KMP Ready
    just JVM

    View full-size slide

  39. https://www.droidcon.com/2022/06/28/embracing-
    commonmain-for-android-development/

    View full-size slide

  40. https://github.com/handstandsam/kmp4free

    View full-size slide

  41. Just Kotlin
    basically

    View full-size slide

  42. kotlin through an objective-c lens

    View full-size slide

  43. Kotlin —> Objc
    lose expressiveness

    View full-size slide

  44. Some Major Highlights
    there are others, but…

    View full-size slide

  45. Default Parameters

    View full-size slide

  46. inline fun i(tag: String = this.tag,
    throwable: Throwable? = null,
    message: () -> String) {
    //etc
    }

    View full-size slide

  47. inline fun i(tag: String = this.tag,
    throwable: Throwable? = null,
    message: () -> String) {
    //etc
    }
    log.i { "Just a string" }

    View full-size slide

  48. inline fun i(tag: String = this.tag,
    throwable: Throwable? = null,
    message: () -> String) {
    //etc
    }
    log.i { "Just a string" }
    log.i(tag: log.tag, throwable: nil) {"A log"}

    View full-size slide

  49. Generics
    it’s complicated

    View full-size slide

  50. No interfaces
    variance gets interesting

    View full-size slide

  51. Coroutines
    also complicated

    View full-size slide

  52. Compiler outputs suspend functions
    we don’t use them (I’ll explain)

    View full-size slide

  53. (Forget Flows)

    View full-size slide

  54. Enums
    not swift-friendly

    View full-size slide

  55. Other Kotlin Structures
    sealed classes, etc

    View full-size slide

  56. Missing Swifty Things
    structs, enums with values

    View full-size slide

  57. fixing the api

    View full-size slide

  58. Two APIs
    direct kotlin and “not kotlin”

    View full-size slide

  59. Common
    Android
    iOS
    Kotlin-friendly API
    Direct calls
    Adapters
    Adapters for non-Kotlin


    Clients

    View full-size slide

  60. Context dependent
    more or less

    View full-size slide

  61. Favor Simple and Specific
    • Simple callbacks (unless you’re doing code gen)
    • Be careful with generics
    • Sealed classes won’t make sense
    • Enums are OK (but not great)

    View full-size slide

  62. Reactive Bridge
    (AKA make coroutines work)

    View full-size slide

  63. Suspend Functions
    we don’t use them

    View full-size slide

  64. Can’t control lifecycle
    no coroutine context

    View full-size slide

  65. Also can’t suppress them
    extra stuff in the interface

    View full-size slide

  66. And, Flows don’t work

    View full-size slide

  67. Several implementations
    us too (kind of)

    View full-size slide

  68. KMP-NativeCoroutines

    View full-size slide

  69. Missing Parallel Features
    in both kotlin and swift

    View full-size slide

  70. Default Parameters
    3 options

    View full-size slide

  71. don’t worry about it (explicit params)
    remove them and have multiple methods
    swift extensions

    View full-size slide

  72. Enums (without data)
    simple adapter

    View full-size slide

  73. Different Language Features
    don’t blame objective-c

    View full-size slide

  74. Kotlin and Swift are different
    syntactic similarities aside

    View full-size slide

  75. Similar Problems
    different solutions

    View full-size slide

  76. Sealed Classes -> Enums
    enums with associated values

    View full-size slide

  77. Structs?
    transform from data classes?

    View full-size slide

  78. Generate Swift?
    for the non-kotlin things

    View full-size slide

  79. Direct Swift Interop
    won’t solve these issues

    View full-size slide

  80. Generics (again)
    yes to interfaces, no to variance

    View full-size slide

  81. A tale of 2 loggers
    mini self-own

    View full-size slide

  82. inline fun i(tag: String = this.tag,
    throwable: Throwable? = null,
    message: () -> String) {
    //etc
    }
    log.i { "Just a string" }
    log.i(tag: log.tag, throwable: nil) {"A log"}

    View full-size slide

  83. inline fun i(message: () -> String) {
    //etc
    }
    inline fun i(throwable: Throwable, message: () -> String) {
    //etc
    }

    View full-size slide

  84. extension [Not calling them out by name…] {


    static func v(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .verbose, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func d(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .debug, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func i(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .info, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func w(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .warning, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func e(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .error, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func a(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .assert, tag: tag, items, separator: separator, file: file, function: function)


    }




    View full-size slide

  85. inline fun i(throwable: Throwable? = null,
    tag: String = this.tag,
    message: () -> String){
    logBlock(Severity.Info, tag, throwable, message)
    }

    View full-size slide

  86. inline fun i(throwable: Throwable? = null,
    tag: String = this.tag,
    message: () -> String){
    logBlock(Severity.Info, tag, throwable, message)
    }

    View full-size slide

  87. extension [Not calling them out by name…] {


    static func v(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .verbose, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func d(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .debug, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func i(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .info, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func w(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .warning, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func e(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .error, tag: tag, items, separator: separator, file: file, function: function)


    }




    static func a(tag: String? = nil, _ items: Any..., separator: String = " ", file: String = #file,
    function: String = #function) {


    log(logLevel: .assert, tag: tag, items, separator: separator, file: file, function: function)


    }




    View full-size slide

  88. This talk is already old
    as I’m writing it

    View full-size slide

  89. This talk is already old
    as I’m writing it

    View full-size slide

  90. Common
    Android
    iOS
    Kotlin-friendly API
    Direct calls
    Adapters
    Adapters for non-Kotlin


    Clients

    View full-size slide

  91. Common
    Android
    iOS
    Kotlin-friendly API
    Direct calls
    (Swift)
    Auto-gen Swift tweaks

    View full-size slide

  92. “swift thing”

    View full-size slide

  93. Swift Generation Library
    with unique parts

    View full-size slide

  94. Swift Templating
    kotlin names aren’t known

    View full-size slide

  95. Swift Linking
    one framework

    View full-size slide

  96. Objc Header Transforms
    shadow stuff

    View full-size slide

  97. Robust Swift Generation

    View full-size slide

  98. When?
    droidcon nyc-ish (so, september)

    View full-size slide

  99. We *may* need support
    pitching friday

    View full-size slide

  100. Swift Only!
    good luck with your JS, etc

    View full-size slide

  101. Practical issues with “2 APIs”
    good and bad stuff

    View full-size slide

  102. Common
    Android
    iOS
    Adapters

    View full-size slide

  103. Common
    Android
    iOS
    Adapters

    View full-size slide

  104. Common
    Android
    iOS
    Adapters
    Export

    View full-size slide

  105. Increasingly Rare
    • Swift gen makes this less useful
    • Some PRs from Rick Clephas fix some of these issues
    directly
    • https://github.com/JetBrains/kotlin/pull/4818 -
    Annotations to suppress exporting Objc/Swift
    methods
    • https://github.com/JetBrains/kotlin/pull/4815 -
    Better Swift naming control

    View full-size slide

  106. Name Prefix
    blunt modularization

    View full-size slide

  107. Common
    Android
    iOS
    Adapters
    MyClass
    CommonMyClass

    View full-size slide

  108. Common
    Android
    iOS
    Adapters
    MyClass
    CommonMyClass
    Export

    View full-size slide

  109. It comes up later
    yes there’s more

    View full-size slide

  110. why you should give talks

    View full-size slide

  111. (or “big tangent time”)

    View full-size slide

  112. “Why am I doing this?”
    thought precedes every talk

    View full-size slide

  113. Explaining forces Examining

    View full-size slide

  114. “2 APIs” is kind of a hack
    and we’re trying to fix it

    View full-size slide

  115. This feels a little inconsistent to me
    all for the better

    View full-size slide

  116. Anyway…
    (just ping me on slack with specific questions)

    View full-size slide

  117. modularization

    View full-size slide

  118. Single Framework
    not a bug

    View full-size slide

  119. TaxCalc
    TaxCalcKt


    Framework
    Products
    ProductsKt


    Framework
    AcmeRepo
    stdlib
    AcmeRepo
    stdlib

    View full-size slide

  120. TaxCalc
    TaxCalcKt


    Framework
    Products
    ProductsKt


    Framework
    AcmeRepo
    stdlib
    AcmeRepo
    stdlib

    View full-size slide

  121. Modularize in Kotlin Layer
    ci can build multiple collection frameworks

    View full-size slide

  122. Name Collisions
    scale makes this worse

    View full-size slide

  123. Swift name has no namespace
    co.touchlab.kermit.Logger —> Logger
    co.touchlab.kermit.helper.Logger —> Logger

    View full-size slide

  124. Swift name has no namespace
    co.touchlab.kermit.Logger —> Logger
    co.touchlab.kermit.helper.Logger —> _Logger

    View full-size slide

  125. Dependency Prefix
    blunt but sort of reduces the problem

    View full-size slide

  126. Multiple Modules
    still one framework

    View full-size slide

  127. multiple modules/namespaces

    View full-size slide

  128. limit exposed api

    View full-size slide

  129. Shared
    Models, Data, Networking
    Shared


    Framework
    Framework from Single Module

    View full-size slide

  130. Shared
    Models, Data, Networking
    Shared


    Framework
    Everything visible, or referenced

    View full-size slide

  131. Shared
    Models, Data,
    Networking
    Shared


    Framework
    Dependencies
    Kermit kotlinx.coroutines SqlDelight
    getLogger():Logger
    getLogger():Logger

    View full-size slide

  132. Shared
    Models, Data,
    Networking
    Shared


    Framework
    Dependencies
    Kermit kotlinx.coroutines SqlDelight
    getLogger():Logger
    getLogger():KermitLogger

    View full-size slide

  133. Shared
    Models, Data,
    Networking
    Shared


    Framework
    Dependencies
    Kermit
    kotlinx.coroutines SqlDelight
    getLogger():Logger
    getLogger():Logger

    View full-size slide

  134. Shared
    Models, Data, Networking
    Shared


    Framework
    Who cares?

    View full-size slide

  135. Bigger header
    hard to parse (for a developer)

    View full-size slide

  136. Bigger binary
    each artifact gets an Objc adapter/logic

    View full-size slide

  137. Non-visible code is still there
    not in header and no extra binary

    View full-size slide

  138. Mark “internal”
    whatever you can

    View full-size slide

  139. Look for dependency refs
    scan the header

    View full-size slide

  140. Consider wrapping things
    for extreme cases

    View full-size slide

  141. Shared
    Models,
    Networking
    Shared


    Framework
    (Maybe) Hide with modules
    Kermit kotlinx.coroutines SqlDelight
    someData():DataMyData
    Data
    someData():MyData

    View full-size slide

  142. Export with caution
    especially transitive

    View full-size slide

  143. Measuring binary size
    not on-disk size

    View full-size slide

  144. Measuring KMP’s impact
    not just “app size”

    View full-size slide

  145. Push to app store
    gold standard

    View full-size slide

  146. Measure locally
    https://docs.flutter.dev/perf/app-size#ios

    View full-size slide

  147. We have a plugin
    just tells you

    View full-size slide

  148. packaging and team

    View full-size slide

  149. Quick Thoughts
    whole different talk

    View full-size slide

  150. Generalizations
    every team is different

    View full-size slide

  151. Just another SDK
    “and it better work!”

    View full-size slide

  152. Common
    iOS
    Framework
    Dev
    Dev App
    Publish (XC)Framework
    Dev
    Big App
    Other Team Consumes (XC)Framework

    View full-size slide

  153. Unwanted Guest
    always gets blamed first

    View full-size slide

  154. Everybody Writes Kotlin
    all new tools, build disruption

    View full-size slide

  155. Common
    iOS
    Framework
    Dev
    Dev App
    Publish (XC)Framework
    Dev
    Big App
    Other Team Consumes (XC)Framework
    iOS Devs
    Android Devs

    View full-size slide

  156. Common
    iOS
    Framework
    Dev
    App
    Publish (XC)Framework

    View full-size slide

  157. Common
    iOS
    Framework
    Dev
    App
    Publish (XC)Framework

    View full-size slide

  158. Common
    iOS
    Framework
    Dev
    App
    Publish (XC)Framework
    iOS Devs
    Android Devs

    View full-size slide

  159. Common
    iOS
    Framework
    Dev
    App
    Publish (XC)Framework
    Mobile Devs
    Mobile Devs

    View full-size slide

  160. Reach out if interested
    see “feedback we want” slide…

    View full-size slide

  161. “everything else”

    View full-size slide

  162. PR’s and YT’s to Watch
    https://github.com/touchlab/KaMPKit/discussions/252

    View full-size slide

  163. Feedback We Want
    Swift generation
    Introducing KMP to your team
    How your iOS code/team is structured

    View full-size slide

  164. Feedback We Want
    https://github.com/touchlab/KaMPKit/discussions/253

    View full-size slide

  165. We’re hiring!
    https://touchlab.co/careers-3/

    View full-size slide

  166. Thanks!
    @kpgalligan

    View full-size slide