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

And that, folks, is how we shared code between ...

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.

Marco Gomiero

February 07, 2021
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

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 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
  4. FOSDEM - @marcoGomier In most of the cases you start

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

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

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

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

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

    methods, aka `object Utils {}` Where to start?
  11. 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
  12. 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
  13. 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
  14. 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)
  15. 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
  16. 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 } ]
  17. 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 } ]
  18. 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
  19. 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
  20. 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
  21. 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" = > "[email protected]" } s.platform = :ios, "10.0" s.ios.vendored_frameworks = 'HNFoundation.framework' # s.swift_version = "4.1" s.source = { :git => "[email protected]: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
  22. FOSDEM - @marcoGomier iOs Project: Podfile # For develop releases:

    pod 'HNFoundation', :git => "[email protected]:prof18/hn-foundation-cocoa.git", :branch => 'develop'
  23. FOSDEM - @marcoGomier iOs Project: Podfile # For develop releases:

    pod 'HNFoundation', :git => "[email protected]:prof18/hn-foundation-cocoa.git", :branch => 'develop' # For stable releases pod 'HNFoundation', :git => "[email protected]:prof18/hn-foundation-cocoa.git", :tag => '1.0.0'
  24. 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? -> !=
  25. 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())
  26. 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 } } } }
  27. 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? -> !=
  28. 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 } } } }
  29. FOSDEM - @marcoGomier Faced [and resolved] difficulties • How to

    organise the project without a monorepo • Gradle • How to effectively distribute the iOs framework
  30. FOSDEM - @marcoGomier Start little then go bigger • We

    have validated the process with “little” effort • Now we can go bigger and share more “features”
  31. 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/
  32. Marco Gomiero Thank you! > Twitter: @marcoGomier 
 > Github:

    prof18 
 > Website: marcogomiero.com Marco Gomiero