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

Introducing Kotlin Multiplatform in an existing project - droidcon Berlin 2021

Introducing Kotlin Multiplatform in an existing project - droidcon Berlin 2021

After discovering a new interesting technology or framework, you will probably start asking yourself how to integrate it into an existing project. That’s because, the possibility to start with a blank canvas is rare (not impossible, but rare).

This is also the case for Kotlin Multiplatform, and even though it is still in alpha, you can already start to use it in production applications.

In this talk, we will understand which part of the code can be a starting point for sharing, how to consume the shared code and how to structure an existing project to have an as smooth as possible integration.

Marco Gomiero

October 22, 2021
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero
    droidcon Berlin
    Introducing


    Kotlin Multiplatform
    in an existing project

    View Slide

  2. droidcon Berlin - @marcoGomier
    Marco Gomiero
    👨💻 Android Engineer @ TIER 🛴 🇩🇪 🇮🇹

    Google Developer Expert for Kotlin
    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com

    View Slide

  3. droidcon Berlin - @marcoGomier
    Kotlin Multiplatform

    View Slide

  4. droidcon Berlin - @marcoGomier
    Kotlin Multiplatform
    • Not about compiling all code for all platforms


    • Share as much [NO UI] code as possible

    View Slide

  5. droidcon Berlin - @marcoGomier
    Common Kotlin
    Kotlin/JVM Kotlin/JS Kotlin/Native
    Java


    Android
    Browser


    NodeJS
    Android NDK


    iOS


    macOS


    watchOS


    tvOS


    Linux


    Windows

    View Slide

  6. droidcon Berlin - @marcoGomier
    Common Kotlin
    Kotlin/JVM Kotlin/JS Kotlin/Native
    Java


    Android
    Browser


    NodeJS
    Android NDK


    iOS


    macOS


    watchOS


    tvOS


    Linux


    Windows
    Mobile App

    View Slide

  7. droidcon Berlin @marcoGomier
    New Project

    View Slide

  8. droidcon Berlin - @marcoGomier
    New Projects: Android Studio plugin

    View Slide

  9. droidcon Berlin - @marcoGomier
    New Projects: Android Studio plugin

    View Slide

  10. droidcon Berlin - @marcoGomier
    droidcon Berlin - @marcoGomier
    .


    └── kmm-project


    ├── androidApp


    ├── iosApp


    └── shared
    Same Repository

    View Slide

  11. droidcon Berlin - @marcoGomier
    shared
    androidApp
    Gradle Module
    Android

    View Slide

  12. droidcon Berlin - @marcoGomier
    //
    settings.gradle.kts


    include(":shared")


    //
    build.gradle.kts


    implementation(project(":shared"))
    Android

    View Slide

  13. droidcon Berlin - @marcoGomier
    shared
    androidApp iosApp
    Gradle Module Framework
    iOS

    View Slide

  14. droidcon Berlin - @marcoGomier
    New Projects: Android Studio plugin

    View Slide

  15. droidcon Berlin - @marcoGomier
    embedAndSignAppleFrameworkForXcode
    https://blog.jetbrains.com/kotlin/2021/07/multiplatform-gradle-plugin-improved-for-connecting-kmm-modules/
    packForXcode
    From Kotlin 1.5.20
    iOS

    View Slide

  16. droidcon Berlin - @marcoGomier

    View Slide

  17. droidcon Berlin - @marcoGomier
    CocoaPods integration
    https://kotlinlang.org/docs/reference/native/cocoapods.html
    Pod
    ::
    Spec.new do |spec|


    spec.name = 'shared'


    spec.version = '1.0-SNAPSHOT'


    spec.homepage = 'Link to a Kotlin/Native module homepage'


    spec.source = { :git
    =>
    "Not Published", :tag
    =
    >
    "Cocoapods/
    #{
    spec.name}
    #{

    spec.authors = ''


    spec.license = ''


    spec.summary = 'Some description for a Kotlin/Native module'


    spec.static_framework = true


    spec.vendored_frameworks = "build/cocoapods/framework/shared.framework"


    spec.libraries = "c
    ++
    "


    spec.module_name = "
    # {
    spec.name}_umbrella"


    spec.pod_target_xcconfig = {


    'KOTLIN_TARGET[sdk=iphonesimulator*]'
    =>
    'ios_x64',


    'KOTLIN_TARGET[sdk=iphoneos*]'
    =>
    'ios_arm',


    'KOTLIN_TARGET[sdk=watchsimulator*]'
    =>
    'watchos_x86',


    'KOTLIN_TARGET[sdk=watchos*]'
    =>
    'watchos_arm',


    'KOTLIN_TARGET[sdk=appletvsimulator*]'
    = >
    'tvos_x64',


    'KOTLIN_TARGET[sdk=appletvos*]'
    =>
    'tvos_arm64',


    'KOTLIN_TARGET[sdk=macosx*]'
    = >
    'macos_x64'


    }


    spec.script_phases = [


    {


    :name
    = >
    'Build shared',


    :execution_position
    = >
    :before_compile,


    :shell_path
    =>
    '/bin/sh',


    :script
    => < <-
    SCRIPT


    set -ev


    REPO_ROOT="$PODS_TARGET_SRCROOT"


    "$REPO_ROOT/
    ..
    /gradlew" -p "$REPO_ROOT" :shared:syncFramework \


    -Pkotlin.native.cocoapods.target=$KOTLIN_TARGET \


    -Pkotlin.native.cocoapods.configuration=$CONFIGURATION \


    -Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \


    -Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \


    -Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"


    SCRIPT


    }


    ]


    end

    View Slide

  18. droidcon Berlin - @marcoGomier
    iOS Project: Podfile
    pod 'shared', :path
    =>
    '
    ..
    /shared'

    View Slide

  19. droidcon Berlin @marcoGomier
    Existing Project

    View Slide

  20. droidcon Berlin - @marcoGomier
    Photo by Ashkan Forouzani on Unsplash


    🙅
    shared
    androidApp iosApp
    Gradle
    Module
    Framework
    Same Repository

    View Slide

  21. droidcon Berlin - @marcoGomier
    Photo by Erwan Hesry on Unsplash
    You can choose a little
    piece of tech stack to
    start with

    View Slide

  22. droidcon Berlin - @marcoGomier
    • Boring code to write multiple times


    • Code/feature that centralises the source of truth (i.e. a field
    is nullable or not)


    • Code/feature that can be gradually extracted
    Where to start?

    View Slide

  23. droidcon Berlin - @marcoGomier
    • DTOs


    • Common Models


    • Utility methods, aka `object Utils {}`


    • Analytics



    . . .
    Where to start?

    View Slide

  24. droidcon Berlin - @marcoGomier
    Existing Projects: Intellij Wizard

    View Slide

  25. droidcon Berlin - @marcoGomier
    Common Kotlin
    Android App iOs App
    .aar Framework

    Maven

    Cocoa Repo

    View Slide

  26. droidcon Berlin - @marcoGomier
    Common Kotlin
    Android App iOs App
    .aar Framework

    Maven

    Cocoa Repo
    Android App Repository
    KMP Repository
    iOs App Repository

    View Slide

  27. droidcon Berlin @marcoGomier
    How to publish: Android

    View Slide

  28. droidcon Berlin - @marcoGomier https://github.com/prof18/shared-hn-android-ios-backend
    * based on a true story project

    View Slide

  29. droidcon Berlin - @marcoGomier
    plugins {


    //..
    .

    id("maven-publish")


    }


    group = "com.prof18.hn.foundation"


    version = "1.0"


    publishing {


    repositories {


    maven{


    credentials {


    username = "username"


    password = "pwd"


    }


    url = url("https:
    //
    mymavenrepo.it")


    }


    }


    }


    Setup a Maven repository to share the artifacts: build.gradle.kts

    View Slide

  30. droidcon Berlin - @marcoGomier
    Publish the artifacts
    ./gradlew publish


    View Slide

  31. droidcon Berlin - @marcoGomier
    Publish the artifacts
    ./gradlew publish


    ./gradlew publishToMavenLocal


    View Slide

  32. droidcon Berlin - @marcoGomier
    Android
    implementation(“com.prof18.hn.foundation:hn-foundation-android:2.0.0")


    View Slide

  33. droidcon Berlin @marcoGomier
    How to publish: iOS

    View Slide

  34. droidcon Berlin - @marcoGomier
    🥵

    View Slide

  35. droidcon Berlin - @marcoGomier
    embedAndSignAppleFrameworkForXcode
    packForXcode
    CocoaPods integration

    View Slide

  36. droidcon Berlin - @marcoGomier
    🤔

    View Slide

  37. droidcon Berlin - @marcoGomier
    XCFramework

    View Slide

  38. droidcon Berlin - @marcoGomier
    NEW
    https://devstreaming-cdn.apple.com/videos/wwdc/2019/416h8485aty341c2/416/416_binary_frameworks_in_swift.pdf

    View Slide

  39. droidcon Berlin - @marcoGomier https://devstreaming-cdn.apple.com/videos/wwdc/2019/416h8485aty341c2/416/416_binary_frameworks_in_swift.pdf
    NEW
    iOS mac
    OS
    watch
    OS
    tvOS

    View Slide

  40. droidcon Berlin - @marcoGomier

    View Slide

  41. droidcon Berlin - @marcoGomier
    https:
    //
    developer.apple.com/videos/play/wwdc2019/416/

    View Slide

  42. droidcon Berlin - @marcoGomier
    XCFramework
    Official Support from Kotlin 1.5.30
    https://kotlinlang.org/docs/whatsnew1530.html#support-for-xcframeworks

    View Slide

  43. XCFramework prior to Kotlin 1.5.30
    https://github.com/prof18/kmp-xcframework-sample/blob/pre-kotlin-1.5.30/build.gradle.kts#L86
    register("buildReleaseXCFramework", Exec
    ::
    class.java) {


    description = "Create a Release XCFramework"


    dependsOn("link${libName}ReleaseFrameworkIosArm64")


    dependsOn("link${libName}ReleaseFrameworkIosX64")


    val arm64FrameworkPath = "$rootDir/build/bin/iosArm64/${libName}ReleaseFramework/${libName}.framework"


    val arm64DebugSymbolsPath =


    "$rootDir/build/bin/iosArm64/${libName}ReleaseFramework/${libName}.framework.dSYM"


    val x64FrameworkPath = "$rootDir/build/bin/iosX64/${libName}ReleaseFramework/${libName}.framework"


    val x64DebugSymbolsPath = "$rootDir/build/bin/iosX64/${libName}ReleaseFramework/${libName}.framework.dSYM"


    val xcFrameworkDest = File("$rootDir/
    ..
    /kmp-xcframework-dest/$libName.xcframework")


    executable = "xcodebuild"


    args(mutableListOf().apply {


    add("-create-xcframework")


    add("-output")


    add(xcFrameworkDest.path)


    //
    Real Device


    add("-framework")


    add(arm64FrameworkPath)


    add("-debug-symbols")


    add(arm64DebugSymbolsPath)


    //
    Simulator


    add("-framework")


    add(x64FrameworkPath)


    add("-debug-symbols")


    add(x64DebugSymbolsPath)


    })


    doFirst {


    xcFrameworkDest.deleteRecursively()


    }


    }

    View Slide

  44. droidcon Berlin - @marcoGomier
    XCFramework prior to Kotlin 1.5.30

    View Slide

  45. droidcon Berlin - @marcoGomier
    XCFramework prior to Kotlin 1.5.30
    https:
    /
    /
    www.marcogomiero.com/posts/2021/build-xcframework-kmp/

    View Slide

  46. droidcon Berlin - @marcoGomier
    XCFramework: build.gradle.kts
    https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts
    import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework


    val libName = "LibraryName"


    kotlin {


    val xcFramework = XCFramework(libName)


    ios {


    binaries.framework(libName) {


    xcFramework.add(this)


    }


    }


    ...

    }

    View Slide

  47. droidcon Berlin - @marcoGomier
    XCFramework: build.gradle.kts
    https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts
    import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework


    val libName = "LibraryName"


    kotlin {


    val xcFramework = XCFramework(libName)


    ios {


    binaries.framework(libName) {


    xcFramework.add(this)


    }


    }


    ...

    }

    View Slide

  48. droidcon Berlin - @marcoGomier
    XCFramework: build.gradle.kts
    https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts
    import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework


    val libName = "LibraryName"


    kotlin {


    val xcFramework = XCFramework(libName)


    ios {


    binaries.framework(libName) {


    xcFramework.add(this)


    }


    }


    ...

    }

    View Slide

  49. droidcon Berlin - @marcoGomier
    XCFramework: build.gradle.kts
    https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts
    import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework


    val libName = "LibraryName"


    kotlin {


    val xcFramework = XCFramework(libName)


    ios {


    binaries.framework(libName) {


    xcFramework.add(this)


    }


    }


    ...

    }

    View Slide

  50. droidcon Berlin - @marcoGomier
    assemble${libName}XCFramework


    assemble${libName}DebugXCFramework


    assemble${libName}ReleaseXCFramework
    XCFramework: Gradle tasks

    View Slide

  51. droidcon Berlin - @marcoGomier
    .


    ├── build


    ├── XCFrameworks


    ├── debug


    │ └── LibraryName.xcframework


    └── release


    └── LibraryName.xcframework


    XCFramework

    View Slide

  52. droidcon Berlin - @marcoGomier https://github.com/prof18/hn-foundation-cocoa-xcframework
    CocoaPod Repository

    View Slide

  53. droidcon Berlin - @marcoGomier
    HNFoundation.podspec


    https://github.com/prof18/hn-foundation-cocoa-xcframework/blob/main/HNFoundation.podspec
    Pod
    ::
    Spec.new do |s|


    # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #


    #


    # These will help people to find your library, and whilst it


    # can feel like a chore to fill in it's definitely to your advantage. The


    # summary should be tweet-length, and the description more in depth.


    #


    s.name = "HNFoundation"


    s.version = "2.0.0"


    s.summary = "HNFoundation KMP library"


    s.homepage = "https:
    //
    github.com/prof18/hn-foundation-cocoa-xcframework"


    s.license = "Apache"


    s.author = { "Marco Gomiero"
    = >
    "[email protected]" }


    s.vendored_frameworks = 'HNFoundation.xcframework'


    s.source = { :git
    =>
    "[email protected]:prof18/hn-foundation-cocoa-xcframework", :tag
    =>
    "
    #{
    s.version}" }


    s.exclude_files = "Classes/Exclude"


    end

    View Slide

  54. View Slide

  55. https:
    //
    guides.cocoapods.org/making/private-cocoapods.html

    View Slide

  56. droidcon Berlin - @marcoGomier
    iOs Project: Podfile
    # For develop releases:


    pod 'HNFoundation', :git
    =>
    [email protected]:prof18/hn-foundation-cocoa-xcframework.git", :branch
    =
    >
    'develop'


    View Slide

  57. droidcon Berlin - @marcoGomier
    iOs Project: Podfile
    # For develop releases:


    pod 'HNFoundation', :git
    =>
    [email protected]:prof18/hn-foundation-cocoa-xcframework.git", :branch
    = >
    'develop'


    # For stable releases


    pod 'HNFoundation', :git
    =>
    [email protected]:prof18/hn-foundation-cocoa-xcframework.git", :tag
    =>
    ‘2.0.0'


    View Slide

  58. register("publishDevFramework") {


    description = "Publish iOs framework to the Cocoa Repo"


    doFirst {


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "checkout", "develop").standardOutput


    }


    }


    dependsOn("assemble${libName}DebugXCFramework")


    doLast {


    copy {


    from("$buildDir/XCFrameworks/debug")


    into("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    }

    .. ..

    https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts#L110
    Publish Dev Framework Task

    View Slide

  59. droidcon Berlin - @marcoGomier


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "checkout", "develop").standardOutput


    }


    }


    dependsOn("assemble${libName}DebugXCFramework")


    doLast {


    copy {


    from("$buildDir/XCFrameworks/debug")


    into("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    }


    val dir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec")


    val tempFile = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec.new")


    val reader = dir.bufferedReader()


    val writer = tempFile.bufferedWriter()


    var currentLine: String?


    while (reader.readLine().also { currLine
    ->
    currentLine = currLine }
    !=
    null) {


    if (currentLine
    ?.
    startsWith("s.version")
    ==
    true) {


    writer.write("s.version = \"${libVersionName}\"" + System.lineSeparator())


    View Slide

  60. droidcon Berlin - @marcoGomier
    into("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    }


    val dir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec")


    val tempFile = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec.new")


    val reader = dir.bufferedReader()


    val writer = tempFile.bufferedWriter()


    var currentLine: String?


    while (reader.readLine().also { currLine
    ->
    currentLine = currLine }
    !=
    null) {


    if (currentLine
    ?.
    startsWith("s.version")
    ==
    true) {


    writer.write("s.version = \"${libVersionName}\"" + System.lineSeparator())


    } else {


    writer.write(currentLine + System.lineSeparator())


    }


    }


    writer.close()


    reader.close()


    val successful = tempFile.renameTo(dir)


    if (successful) {


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    View Slide

  61. droidcon Berlin - @marcoGomier
    if (successful) {


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "add", ".").standardOutput


    }


    val dateFormatter = SimpleDateFormat("dd/MM/yyyy - HH:mm", Locale.getDefault())


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine(


    "git",


    "commit",


    "-m",


    "\"New dev release: ${libVersionName}-${dateFormatter.format(Date())}\""


    ).standardOutput


    }


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "push", "origin", "develop").standardOutput


    }


    }




    View Slide

  62. https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts#L177
    Publish Release Framework Task
    register("publishFramework") {


    description = "Publish iOs framework to the Cocoa Repo"


    doFirst {


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "checkout", "main").standardOutput


    }


    }


    dependsOn("assemble${libName}ReleaseXCFramework")


    doLast {


    copy {


    from("$buildDir/XCFrameworks/release")


    into("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    }

    .. ..

    View Slide

  63. droidcon Berlin - @marcoGomier
    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "checkout", "main").standardOutput


    }


    }


    dependsOn("assemble${libName}ReleaseXCFramework")


    doLast {


    copy {


    from("$buildDir/XCFrameworks/release")


    into("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    }


    val dir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec")


    val tempFile = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework/$libName.podspec.new")


    val reader = dir.bufferedReader()


    val writer = tempFile.bufferedWriter()


    var currentLine: String?


    while (reader.readLine().also { currLine
    ->
    currentLine = currLine }
    !=
    null) {


    if (currentLine
    ?.
    startsWith("s.version")
    ==
    true) {



    View Slide

  64. droidcon Berlin - @marcoGomier
    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "add", ".").standardOutput


    }


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "commit", "-m", "\"New release: ${libVersionName}\"").standardOu


    }


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "tag", libVersionName).standardOutput


    }


    project.exec {


    workingDir = File("$rootDir/
    ..
    /
    ..
    /hn-foundation-cocoa-xcframework")


    commandLine("git", "push", "origin", "main", "
    --
    tags").standardOutput


    }


    }


    }


    }


    View Slide

  65. droidcon Berlin - @marcoGomier
    🎉

    View Slide

  66. droidcon Berlin @marcoGomier
    Concurrency

    View Slide

  67. droidcon Berlin - @marcoGomier
    • Kotlin/Native different from the JVM


    • One thread
    ->
    no problems, you can use mutable objects


    • Many thread
    -
    >
    only immutable objects
    Concurrency
    https://kotlinlang.org/docs/mobile/concurrency-overview.html

    View Slide

  68. Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException:
    illegal attempt to access non-shared @c2ddb8 from other thread


    View Slide

  69. droidcon Berlin - @marcoGomier
    🥶 Freeze the object to share
    it between threads

    View Slide

  70. droidcon Berlin - @marcoGomier
    override fun deserialize(jsonString: String): NewsDTO {


    val newsDTO: NewsDTO = json.decodeFromString(jsonString)


    newsDTO.freeze()


    return newsDTO


    }

    View Slide

  71. droidcon Berlin - @marcoGomier https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/

    View Slide

  72. droidcon Berlin @marcoGomier
    Conclusions

    View Slide

  73. droidcon Berlin - @marcoGomier
    Faced [and resolved] difficulties
    • How to organise the project without a monorepo


    • How to effectively distribute the iOS framework

    View Slide

  74. droidcon Berlin - @marcoGomier
    Start little

    View Slide

  75. droidcon Berlin - @marcoGomier
    Start little
    Go bigger
    then

    View Slide

  76. droidcon Berlin - @marcoGomier
    Start little then go bigger
    • Validate the process with “little” effort


    • Then you can go bigger and share more “features”

    View Slide

  77. Bibliography / Useful Links
    • https:
    / /
    kotlinlang.org/lp/mobile/


    • https:
    / /
    www.marcogomiero.com/posts/2020/my-2cents-cross-platform/


    • https:
    / /
    giansegato.com/essays/a-technical-framework-for-early-stage-startups/


    • https:
    / /
    giansegato.com/essays/the-ebb-and-the-flow-of-product-development/


    • https:
    / /
    www.marcogomiero.com/posts/2021/build-xcframework-kmp/


    • https:
    / /
    www.marcogomiero.com/posts/2021/kmp-xcframework-official-support/


    • https:
    / /
    www.marcogomiero.com/posts/2020/kotlin-serialization-alamofire/


    • https:
    / /
    www.marcogomiero.com/posts/2021/kmp-existing-project/


    • https:
    / /
    www.youtube.com/watch?v=oxQ6e1VeH4M


    • https:
    / /
    dev.to/kpgalligan/series/3739


    • https:
    / /
    blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-
    development-preview/


    View Slide

  78. Marco Gomiero
    droidcon Berlin
    Thank you!


    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com

    View Slide