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

And that, folks, is how we shared code between Android, iOS and the Backend | FOSDEM

9da5d5cc4b6a9f28058152e28364b02a?s=47 Marco Gomiero
February 07, 2021

And that, folks, is how we shared code between Android, iOS and the Backend | FOSDEM

Kotlin Multiplatform is an alpha feature that you can use to share code between different platforms. Even if it is in alpha stage, it is already possible to start using it in production applications.

In this talk, I will share the discussion that led us to Kotlin Multiplatform, and the following processes we put in place to start using it in production for an Android, iOS, and backend project. I will show you what parts of the code you can (gradually) start to share and how to integrate with existing standalone projects.

9da5d5cc4b6a9f28058152e28364b02a?s=128

Marco Gomiero

February 07, 2021
Tweet

Transcript

  1. Marco Gomiero And that, folks, is how we shared code

    between Android, iOS and the Backend FOSDEM
  2. FOSDEM - @marcoGomier Marco Gomiero 👨💻 Tech Lead @ Uniwhere

    🇩🇪 🇮🇹 
 🍻 Co-Lead @ GDG Venezia > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com
  3. FOSDEM @marcoGomier Kotlin Multiplatform @marcoGomier

  4. FOSDEM - @marcoGomier Not about compiling all code for 


    all platforms Kotlin Multiplatform
  5. FOSDEM - @marcoGomier Not about compiling all code for 


    all platforms Share as much [NO UI] code as possible https://kotlinlang.org/docs/reference/multiplatform.html Kotlin Multiplatform Unifying UI declaration between platform is complicated
  6. FOSDEM @marcoGomier How to approach @marcoGomier

  7. FOSDEM - @marcoGomier In most of the cases you start

    from an existing project Photo by Jonas Jacobsson on Unsplash
  8. FOSDEM - @marcoGomier You don’t have enough time for a

    big rewrite Photo by Ashkan Forouzani on Unsplash
  9. FOSDEM - @marcoGomier Conference Name - @marcoGomier . └── kmm-project

    ├── androidApp ├── iosApp └── shared Photo by Ashkan Forouzani on Unsplash 🙅
  10. FOSDEM - @marcoGomier Photo by Erwan Hesry on Unsplash You

    can choose a little piece of tech stack to start with
  11. FOSDEM - @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?
  12. FOSDEM - @marcoGomier • DTOs • Common Models • Utility

    methods, aka `object Utils {}` Where to start?
  13. FOSDEM - @marcoGomier • DTOs • Common Models • Utility

    methods, aka `object Utils {}` Where to start?
  14. FOSDEM @marcoGomier Let’s start sharing @marcoGomier

  15. FOSDEM - @marcoGomier Common Kotlin .aar Framework Maven Repo .jar

    iOs App iOs App Repository Android App Android App Repository Backend Backend Repository KMP Repository Cocoa Repo Architecture
  16. FOSDEM - @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") } } } Maven repository to share the artifacts
  17. FOSDEM - @marcoGomier Android implementation("com.prof18.hn.foundation:hn-foundation-android:1.0.0")

  18. FOSDEM - @marcoGomier Backend implementation("com.prof18.hn.foundation:hn-foundation-jvm:1.0.0")

  19. FOSDEM - @marcoGomier  And iOs?

  20. FOSDEM - @marcoGomier 🥵

  21. FOSDEM - @marcoGomier packForXcode val packForXcode by tasks.creating(Sync :: class)

    { group = "build" val mode = System.getenv("CONFIGURATION") ?: "DEBUG" val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64" val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode) inputs.property("mode", mode) dependsOn(framework.linkTask) val targetDir = File(buildDir, "xcode-frameworks") from({ framework.outputDirectory }) into(targetDir) } tasks.getByName("build").dependsOn(packForXcode) Auto generated when creating a Kotlin Multiplatform mobile app
  22. FOSDEM - @marcoGomier packForXcode Auto generated when creating a Kotlin

    Multiplatform mobile app val packForXcode by tasks.creating(Sync :: class) { group = "build" val mode = System.getenv("CONFIGURATION") ?: "DEBUG" val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64" val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode) inputs.property("mode", mode) dependsOn(framework.linkTask) val targetDir = File(buildDir, "xcode-frameworks") from({ framework.outputDirectory }) into(targetDir) } tasks.getByName("build").dependsOn(packForXcode)
  23. FOSDEM - @marcoGomier CocoaPods plugin plugins { kotlin("multiplatform") version "1.4.10"

    kotlin("native.cocoapods") version "1.4.10" } // CocoaPods requires the podspec to have a version. version = "1.0" kotlin { cocoapods { // Configure fields required by CocoaPods. summary = "Some description for a Kotlin/Native module" homepage = "Link to a Kotlin/Native module homepage" // You can change the name of the produced framework. // By default, it is the name of the Gradle project. frameworkName = "my_framework" } } https://kotlinlang.org/docs/reference/native/cocoapods.html
  24. FOSDEM - @marcoGomier CocoaPods plugin: shared.podspec https://kotlinlang.org/docs/reference/native/cocoapods.html 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 } ]
  25. FOSDEM - @marcoGomier CocoaPods plugin: shared.podspec https://kotlinlang.org/docs/reference/native/cocoapods.html 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 } ]
  26. FOSDEM - @marcoGomier 🤔

  27. FOSDEM - @marcoGomier Fat Framework

  28. FOSDEM - @marcoGomier org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask :: class

  29. FOSDEM - @marcoGomier Custom Gradle Task: universalFrameworkDebug val libName =

    "HNFoundation" tasks { register("universalFrameworkDebug", org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask :: class) { baseName = libName from( iosArm64().binaries.getFramework(libName, "Debug"), iosX64().binaries.getFramework(libName, "Debug") ) destinationDir = buildDir.resolve("$rootDir/ .. / .. /hn-foundation-cocoa") group = libName description = "Create the debug framework for iOs" dependsOn("linkHNFoundationDebugFrameworkIosArm64") dependsOn("linkHNFoundationDebugFrameworkIosX64") } ... } https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/build.gradle.kts#L100
  30. FOSDEM - @marcoGomier Custom Gradle Task: universalFrameworkDebug val libName =

    "HNFoundation" tasks { register("universalFrameworkDebug", org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask :: class) { baseName = libName from( iosArm64().binaries.getFramework(libName, "Debug"), iosX64().binaries.getFramework(libName, "Debug") ) destinationDir = buildDir.resolve("$rootDir/ .. / .. /hn-foundation-cocoa") group = libName description = "Create the debug framework for iOs" dependsOn("linkHNFoundationDebugFrameworkIosArm64") dependsOn("linkHNFoundationDebugFrameworkIosX64") } ... } https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/build.gradle.kts#L100
  31. FOSDEM - @marcoGomier Custom Gradle Task: universalFrameworkDebug val libName =

    "HNFoundation" tasks { register("universalFrameworkDebug", org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask :: class) { baseName = libName from( iosArm64().binaries.getFramework(libName, "Debug"), iosX64().binaries.getFramework(libName, "Debug") ) destinationDir = buildDir.resolve("$rootDir/ .. / .. /hn-foundation-cocoa") group = libName description = "Create the debug framework for iOs" dependsOn("linkHNFoundationDebugFrameworkIosArm64") dependsOn("linkHNFoundationDebugFrameworkIosX64") } ... } https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/build.gradle.kts#L100
  32. FOSDEM - @marcoGomier https://github.com/prof18/hn-foundation-cocoa/ CocoaPod Repository

  33. FOSDEM - @marcoGomier 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 = "1.0.0" s.summary = "HNFoundation KMP library" s.homepage = "https: // github.com/prof18/hn-foundation-cocoa" s.description = "The framework of the HNFoundation library" s.license = "UNLICENSED" s.author = { "Marco Gomiero" = > "mg@mail.it" } s.platform = :ios, "10.0" s.ios.vendored_frameworks = 'HNFoundation.framework' # s.swift_version = "4.1" s.source = { :git => "git@github.com:prof18/hn-foundation-cocoa.git", :tag = > " #{ s.version}" } s.exclude_files = "Classes/Exclude" end HNFoundation.podspec https://github.com/prof18/hn-foundation-cocoa/blob/master/HNFoundation.podspec
  34. None
  35. https: / / guides.cocoapods.org/making/private- cocoapods.html

  36. FOSDEM - @marcoGomier iOs Project: Podfile # For develop releases:

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

    pod 'HNFoundation', :git => "git@github.com:prof18/hn-foundation-cocoa.git", :branch => 'develop' # For stable releases pod 'HNFoundation', :git => "git@github.com:prof18/hn-foundation-cocoa.git", :tag => '1.0.0'
  38. iOs Publish Dev Task https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/build.gradle.kts#L132 register("publishDevFramework") { description = "Publish

    iOs framweork to the Cocoa Repo" project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "checkout", "develop").standardOutput } // Create Release Framework for Xcode dependsOn("universalFrameworkDebug") // Replace doLast { val dir = File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.podspec") val tempFile = File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.podspec.new") val reader = dir.bufferedReader() val writer = tempFile.bufferedWriter() var currentLine: String? -> !=
  39. FOSDEM - @marcoGomier // Replace doLast { val dir =

    File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.podspec") val tempFile = File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.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) { val dateFormatter = SimpleDateFormat("dd/MM/yyyy - HH:mm", Locale.getDefault())
  40. FOSDEM - @marcoGomier writer.write(currentLine + System.lineSeparator()) } } writer.close() reader.close()

    val successful = tempFile.renameTo(dir) if (successful) { val dateFormatter = SimpleDateFormat("dd/MM/yyyy - HH:mm", Locale.getDefault()) project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "commit", "-a", "-m", "\"New dev release: ${libVersionName}-${dateFormatter.format(Date())}\"" ).standardOutput } project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "push", "origin", "develop").standardOutput } } } }
  41. iOs Publish Release Task https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/build.gradle.kts#L189 register("publishFramework") { description = "Publish

    iOs framework to the Cocoa Repo" project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "checkout", "master").standardOutput } // Create Release Framework for Xcode dependsOn("universalFrameworkRelease") // Replace doLast { val dir = File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.podspec") val tempFile = File("$rootDir/ .. / .. /hn-foundation-cocoa/HNFoundation.podspec.new") val reader = dir.bufferedReader() val writer = tempFile.bufferedWriter() var currentLine: String? -> !=
  42. FOSDEM - @marcoGomier reader.close() val successful = tempFile.renameTo(dir) if (successful)

    { project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "commit", "-a", "-m", "\"New release: ${libVersionName}\"").standardOutput } project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "tag", libVersionName).standardOutput } project.exec { workingDir = File("$rootDir/ .. / .. /hn-foundation-cocoa") commandLine("git", "push", "origin", "master", " -- tags").standardOutput } } } }
  43. FOSDEM - @marcoGomier 🎉

  44. FOSDEM - @marcoGomier https://github.com/prof18/shared-hn-android-ios-backend Sample Code

  45. FOSDEM @marcoGomier Conclusions @marcoGomier

  46. FOSDEM - @marcoGomier Faced [and resolved] difficulties • How to

    organise the project without a monorepo • Gradle • How to effectively distribute the iOs framework
  47. FOSDEM - @marcoGomier Start little

  48. FOSDEM - @marcoGomier Start little Go bigger then

  49. FOSDEM - @marcoGomier Start little then go bigger • We

    have validated the process with “little” effort • Now we can go bigger and share more “features”
  50. Bibliography / Useful Links • 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.youtube.com/watch?v=oxQ6e1VeH4M • https: / / dev.to/kpgalligan/series/3739 • https: / / blog.jetbrains.com/kotlin/2020/07/kotlin-native- memory-management-roadmap/ • https: / / dev.to/touchlab/kotlin-native-concurrency-changes- p3e • https: / / kotlinlang.org/lp/mobile/
  51. Marco Gomiero Thank you! > Twitter: @marcoGomier 
 > Github:

    prof18 
 > Website: marcogomiero.com Marco Gomiero