Save 37% off PRO during our Black Friday Sale! »

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.

9da5d5cc4b6a9f28058152e28364b02a?s=128

Marco Gomiero

October 22, 2021
Tweet

Transcript

  1. Marco Gomiero droidcon Berlin Introducing Kotlin Multiplatform in an existing

    project
  2. droidcon Berlin - @marcoGomier Marco Gomiero 👨💻 Android Engineer @

    TIER 🛴 🇩🇪 🇮🇹 
 Google Developer Expert for Kotlin > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com
  3. droidcon Berlin - @marcoGomier Kotlin Multiplatform

  4. droidcon Berlin - @marcoGomier Kotlin Multiplatform • Not about compiling

    all code for all platforms • Share as much [NO UI] code as possible
  5. droidcon Berlin - @marcoGomier Common Kotlin Kotlin/JVM Kotlin/JS Kotlin/Native Java

    Android Browser NodeJS Android NDK iOS macOS watchOS tvOS Linux Windows
  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
  7. droidcon Berlin @marcoGomier New Project

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

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

  10. droidcon Berlin - @marcoGomier droidcon Berlin - @marcoGomier . └──

    kmm-project ├── androidApp ├── iosApp └── shared Same Repository
  11. droidcon Berlin - @marcoGomier shared androidApp Gradle Module Android

  12. droidcon Berlin - @marcoGomier // settings.gradle.kts include(":shared") // build.gradle.kts implementation(project(":shared"))

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

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

  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
  16. droidcon Berlin - @marcoGomier

  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
  18. droidcon Berlin - @marcoGomier iOS Project: Podfile pod 'shared', :path

    => ' .. /shared'
  19. droidcon Berlin @marcoGomier Existing Project

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

    🙅 shared androidApp iosApp Gradle Module Framework Same Repository
  21. droidcon Berlin - @marcoGomier Photo by Erwan Hesry on Unsplash

    You can choose a little piece of tech stack to start with
  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?
  23. droidcon Berlin - @marcoGomier • DTOs • Common Models •

    Utility methods, aka `object Utils {}` • Analytics • . . . Where to start?
  24. droidcon Berlin - @marcoGomier Existing Projects: Intellij Wizard

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

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

    .aar Framework 
 Maven 
 Cocoa Repo Android App Repository KMP Repository iOs App Repository
  27. droidcon Berlin @marcoGomier How to publish: Android

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

    story project
  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
  30. droidcon Berlin - @marcoGomier Publish the artifacts ./gradlew publish

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

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

  33. droidcon Berlin @marcoGomier How to publish: iOS

  34. droidcon Berlin - @marcoGomier 🥵

  35. droidcon Berlin - @marcoGomier embedAndSignAppleFrameworkForXcode packForXcode CocoaPods integration

  36. droidcon Berlin - @marcoGomier 🤔

  37. droidcon Berlin - @marcoGomier XCFramework

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

  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
  40. droidcon Berlin - @marcoGomier

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

  42. droidcon Berlin - @marcoGomier XCFramework Official Support from Kotlin 1.5.30

    https://kotlinlang.org/docs/whatsnew1530.html#support-for-xcframeworks
  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<String>().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() } }
  44. droidcon Berlin - @marcoGomier XCFramework prior to Kotlin 1.5.30

  45. droidcon Berlin - @marcoGomier XCFramework prior to Kotlin 1.5.30 https:

    / / www.marcogomiero.com/posts/2021/build-xcframework-kmp/
  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) } } ... }
  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) } } ... }
  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) } } ... }
  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) } } ... }
  50. droidcon Berlin - @marcoGomier assemble${libName}XCFramework assemble${libName}DebugXCFramework assemble${libName}ReleaseXCFramework XCFramework: Gradle tasks

  51. droidcon Berlin - @marcoGomier . ├── build ├── XCFrameworks ├──

    debug │ └── LibraryName.xcframework └── release └── LibraryName.xcframework XCFramework
  52. droidcon Berlin - @marcoGomier https://github.com/prof18/hn-foundation-cocoa-xcframework CocoaPod Repository

  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" = > "mg@me.com" } s.vendored_frameworks = 'HNFoundation.xcframework' s.source = { :git => "git@github.com:prof18/hn-foundation-cocoa-xcframework", :tag => " #{ s.version}" } s.exclude_files = "Classes/Exclude" end
  54. None
  55. https: // guides.cocoapods.org/making/private-cocoapods.html

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

    releases: pod 'HNFoundation', :git => “git@github.com:prof18/hn-foundation-cocoa-xcframework.git", :branch = > 'develop'
  57. droidcon Berlin - @marcoGomier iOs Project: Podfile # For develop

    releases: pod 'HNFoundation', :git => “git@github.com:prof18/hn-foundation-cocoa-xcframework.git", :branch = > 'develop' # For stable releases pod 'HNFoundation', :git => “git@github.com:prof18/hn-foundation-cocoa-xcframework.git", :tag => ‘2.0.0'
  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
  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())
  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")
  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 } }
  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") } .. ..
  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) {
  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 } } } }
  65. droidcon Berlin - @marcoGomier 🎉

  66. droidcon Berlin @marcoGomier Concurrency

  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
  68. Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared <object>@c2ddb8

    from other thread
  69. droidcon Berlin - @marcoGomier 🥶 Freeze the object to share

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

    val newsDTO: NewsDTO = json.decodeFromString(jsonString) newsDTO.freeze() return newsDTO }
  71. droidcon Berlin - @marcoGomier https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/

  72. droidcon Berlin @marcoGomier Conclusions

  73. droidcon Berlin - @marcoGomier Faced [and resolved] difficulties • How

    to organise the project without a monorepo • How to effectively distribute the iOS framework
  74. droidcon Berlin - @marcoGomier Start little

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

  76. droidcon Berlin - @marcoGomier Start little then go bigger •

    Validate the process with “little” effort • Then you can go bigger and share more “features” 

  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/
  78. Marco Gomiero droidcon Berlin Thank you! > Twitter: @marcoGomier 


    > Github: prof18 
 > Website: marcogomiero.com