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

Introducing Kotlin Multiplatform in an existing project - DevFest Romania

Introducing Kotlin Multiplatform in an existing project - DevFest Romania

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

November 13, 2021
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero DevFest Romania Introducing Kotlin Multiplatform in an existing

    project
  2. DevFest Romania - @marcoGomier Marco Gomiero 👨💻 Android Engineer @

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

  4. DevFest Romania - @marcoGomier Kotlin Multiplatform • Not about compiling

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

    Android Browser NodeJS Android NDK iOS macOS watchOS tvOS Linux Windows
  6. DevFest Romania - @marcoGomier Common Kotlin Kotlin/JVM Kotlin/JS Kotlin/Native Java

    Android Browser NodeJS Android NDK iOS macOS watchOS tvOS Linux Windows Mobile App
  7. DevFest Romania @marcoGomier New Project

  8. DevFest Romania - @marcoGomier New Projects: Android Studio plugin

  9. DevFest Romania - @marcoGomier New Projects: Android Studio plugin

  10. DevFest Romania - @marcoGomier DevFest Romania - @marcoGomier . └──

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

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

    Android
  13. DevFest Romania - @marcoGomier shared androidApp iosApp Gradle Module Framework

    iOS
  14. DevFest Romania - @marcoGomier New Projects: Android Studio plugin

  15. DevFest Romania - @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. DevFest Romania - @marcoGomier

  17. DevFest Romania - @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. DevFest Romania - @marcoGomier iOS Project: Podfile pod 'shared', :path

    => ' .. /shared'
  19. DevFest Romania @marcoGomier Existing Project

  20. DevFest Romania - @marcoGomier Photo by Ashkan Forouzani on Unsplash

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

    💡 Create a library!
  22. DevFest Romania - @marcoGomier Photo by Erwan Hesry on Unsplash

    A little piece of tech stack to start with
  23. DevFest Romania - @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?
  24. DevFest Romania - @marcoGomier • DTOs • Common Models •

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

  26. DevFest Romania - @marcoGomier Common Kotlin Android App iOs App

    .aar Framework 
 Maven 
 Cocoa Repo
  27. DevFest Romania - @marcoGomier Common Kotlin Android App iOs App

    .aar Framework 
 Maven 
 Cocoa Repo Android App Repository KMP Repository iOs App Repository
  28. DevFest Romania @marcoGomier How to publish: Android

  29. DevFest Romania - @marcoGomier https://github.com/prof18/shared-hn-android-ios-backend * based on a true

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

  32. DevFest Romania - @marcoGomier Publish the artifacts ./gradlew publish ./gradlew

    publishToMavenLocal
  33. DevFest Romania - @marcoGomier Android implementation(“com.prof18.hn.foundation:hn-foundation-android:2.0.0")

  34. DevFest Romania @marcoGomier How to publish: iOS

  35. DevFest Romania - @marcoGomier 🥵

  36. DevFest Romania - @marcoGomier embedAndSignAppleFrameworkForXcode packForXcode CocoaPods integration

  37. DevFest Romania - @marcoGomier 🤔

  38. DevFest Romania - @marcoGomier XCFramework

  39. DevFest Romania - @marcoGomier NEW https://devstreaming-cdn.apple.com/videos/wwdc/2019/416h8485aty341c2/416/416_binary_frameworks_in_swift.pdf

  40. DevFest Romania - @marcoGomier https://devstreaming-cdn.apple.com/videos/wwdc/2019/416h8485aty341c2/416/416_binary_frameworks_in_swift.pdf NEW iOS mac OS watch

    OS tvOS
  41. DevFest Romania - @marcoGomier

  42. DevFest Romania - @marcoGomier https: // developer.apple.com/videos/play/wwdc2019/416/

  43. DevFest Romania - @marcoGomier XCFramework Official Support from Kotlin 1.5.30

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

  46. DevFest Romania - @marcoGomier XCFramework prior to Kotlin 1.5.30 https:

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

  52. DevFest Romania - @marcoGomier . ├── build ├── XCFrameworks ├──

    debug │ └── LibraryName.xcframework └── release └── LibraryName.xcframework XCFramework
  53. DevFest Romania - @marcoGomier https://github.com/prof18/hn-foundation-cocoa-xcframework CocoaPod Repository

  54. DevFest Romania - @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
  55. None
  56. https: // guides.cocoapods.org/making/private-cocoapods.html

  57. DevFest Romania - @marcoGomier iOs Project: Podfile # For develop

    releases: pod 'HNFoundation', :git => “git@github.com:prof18/hn-foundation-cocoa-xcframework.git", :branch = > 'develop'
  58. DevFest Romania - @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'
  59. 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
  60. DevFest Romania - @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())
  61. DevFest Romania - @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")
  62. DevFest Romania - @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 } }
  63. 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") } .. ..
  64. DevFest Romania - @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) {
  65. DevFest Romania - @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 } } } }
  66. DevFest Romania - @marcoGomier 🎉

  67. DevFest Romania @marcoGomier Concurrency

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

    from other thread
  70. DevFest Romania - @marcoGomier 🥶 Freeze the object to share

    it between threads
  71. DevFest Romania - @marcoGomier override fun deserialize(jsonString: String): NewsDTO {

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

  73. DevFest Romania @marcoGomier Conclusions

  74. DevFest Romania - @marcoGomier Faced [and resolved] difficulties • How

    to organise the project without a monorepo • How to effectively distribute the iOS framework
  75. DevFest Romania - @marcoGomier Start little

  76. DevFest Romania - @marcoGomier Start little Go bigger then

  77. DevFest Romania - @marcoGomier Start little then go bigger •

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

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


    > Github: prof18 
 > Website: marcogomiero.com