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 | droidcon EMEA

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

Kotlin Multiplatform is an experimental feature that you can use to share code between different platforms. Even if it is an experimental feature, 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

October 08, 2020
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. droidcon EMEA Marco Gomiero And that, folks, is how we

    shared code between Android, iOS and the Backend
  2. droidcon EMEA - @marcoGomier Marco Gomiero Tech Lead @ Uniwhere

    Co-Lead @ GDG Venezia > Twitter: @marcoGomier > Github: prof18 > Website: marcogomiero.com
  3. droidcon EMEA - @marcoGomier Alice Mobile Developer data class NewsDTO(

    val id: Long, val author: String ) data class NewsDTO( val id: Long, val author: String? ) OR
  4. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

    Is author nullable? Mmm, the doc says its not!
  5. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

    Well, its not working as expected Let me check
  6. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

    Oh, you’re right! Sorry! I’ve changed the value last day!
  7. droidcon EMEA - @marcoGomier Alice Mobile Developer Charlie Product Manager

    Can you now implement this logic on iOs as well? Sure!
  8. droidcon EMEA - @marcoGomier How to tackle • Cross Platform

    solutions (React Native, Flutter, "..)
  9. droidcon EMEA - @marcoGomier How to tackle • Cross Platform

    solutions (React Native, Flutter, "..)
  10. droidcon EMEA - @marcoGomier Cross Platform solutions (React Native, Flutter,

    "..) Unifying UI declaration between platform is complicated
  11. droidcon EMEA - @marcoGomier Cross Platform solutions (React Native, Flutter,

    "..) Unifying UI declaration between platform is complicated Different platforms have different patterns
  12. droidcon EMEA - @marcoGomier How to tackle • Cross Platform

    solutions (React Native, Flutter, "..) • Kotlin Multiplatform
  13. droidcon EMEA - @marcoGomier Not about compiling all code for

    all platforms Kotlin Multiplatform Share as much [NO UI] code as possible https://kotlinlang.org/docs/reference/multiplatform.html
  14. droidcon EMEA - @marcoGomier In most of the cases you

    start from an existing project Photo by Jonas Jacobsson on Unsplash
  15. droidcon EMEA - @marcoGomier You don’t have enough time for

    a big rewrite Photo by Ashkan Forouzani on Unsplash
  16. droidcon EMEA - @marcoGomier Conference Name - @marcoGomier . └──

    kmm-project ├── androidApp ├── iosApp └── shared Photo by Ashkan Forouzani on Unsplash
  17. droidcon EMEA - @marcoGomier Photo by Erwan Hesry on Unsplash

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

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

    Utility methods, aka `object Utils {}` Where to start?
  21. droidcon EMEA - @marcoGomier • Create a new project •

    Add the JVM target [if needed] Let’s start building
  22. droidcon EMEA - @marcoGomier Let’s start building • Create a

    new project • Add the JVM target [if needed] • Setup a Maven repository to share the artifacts
  23. droidcon EMEA - @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
  24. droidcon EMEA - @marcoGomier • Create a new project •

    Add the JVM target [if needed] • Setup a Maven repository to share the artifacts • Write some code! Let’s start building
  25. droidcon EMEA - @marcoGomier NewsDTO.kt data class NewsDTO( val author:

    String, val id: Long, val score: Int, val timestamp: Long, val title: String, val type: String, val url: String )
  26. droidcon EMEA - @marcoGomier • Create a new project •

    Add the JVM target [if needed] • Setup a Maven repository to share the artifacts • Write some code! Let’s start building • Add the new library to the backend and the clients
  27. droidcon EMEA - @marcoGomier ! Backend routing { get("/hn/topStories") {

    call.respond(getTopStories()) } } fun getTopStories(): List<NewsDTO> { ""... }
  28. droidcon EMEA - @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
  29. droidcon EMEA - @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)
  30. droidcon EMEA - @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
  31. droidcon EMEA - @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 } ]
  32. droidcon EMEA - @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 } ]
  33. droidcon EMEA - @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
  34. droidcon EMEA - @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
  35. droidcon EMEA - @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
  36. droidcon EMEA - @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
  37. droidcon EMEA - @marcoGomier iOs Project: Podfile # For develop

    releases: pod 'HNFoundation', :git "=> "[email protected]:prof18/hn-foundation-cocoa.git", :branch "=> 'develop'
  38. droidcon EMEA - @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'
  39. 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?
  40. droidcon EMEA - @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())
  41. droidcon EMEA - @marcoGomier } 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 } } } }
  42. 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?
  43. droidcon EMEA - @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 } } } }
  44. droidcon EMEA - @marcoGomier How to handle deserialization? • Probably

    you are using Alamofire for net handling • [De]Serialization probable handled with the Codable or other solutions like ObjectMapper
  45. droidcon EMEA - @marcoGomier How to handle deserialization? • Probably

    you are using Alamofire for net handling • [De]Serialization probable handled with the Codable or other solutions like ObjectMapper • Need to comply to the Decodable or the Mappable protocol
  46. droidcon EMEA - @marcoGomier NewsDTODecodable.swift class NewsDTODecodable: NewsDTO, Decodable {

    public required init(from decoder: Decoder) throws { super.init() let container = try decoder.container(keyedBy: CodingKeys.self) author = try container.decode(String.self, forKey: .author) id = try container.decode(Int64.self, forKey: .id) score = try container.decode(Int32.self, forKey: .score) timestamp = try container.decode(Int64.self, forKey: .timestamp) title = try container.decode(String.self, forKey: .title) type = try container.decode(String.self, forKey: .type) url = try container.decode(String.self, forKey: .url) } enum CodingKeys: String, CodingKey { case author case id case score case timestamp
  47. droidcon EMEA - @marcoGomier NewsDTO.kt https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/src/commonMain/kotlin/com/prof18/hn/dto/NewsDTO.kt open class NewsDTO( var

    author: String, var id: Long, var score: Int, var timestamp: Long, var title: String, var type: String, var url: String ): { constructor() : this( ".. ) { ".. } }
  48. droidcon EMEA - @marcoGomier NewsDTODecodable.swift class NewsDTODecodable: NewsDTO, Decodable {

    public required init(from decoder: Decoder) throws { super.init() let container = try decoder.container(keyedBy: CodingKeys.self) author = try container.decode(String.self, forKey: .author) id = try container.decode(Int64.self, forKey: .id) score = try container.decode(Int32.self, forKey: .score) timestamp = try container.decode(Int64.self, forKey: .timestamp) title = try container.decode(String.self, forKey: .title) type = try container.decode(String.self, forKey: .type) url = try container.decode(String.self, forKey: .url) super.makeFrozen() } enum CodingKeys: String, CodingKey { case author case id
  49. droidcon EMEA - @marcoGomier Faced [and resolved] difficulties • How

    to organise the project without a monorepo • Gradle • How to effectively distribute the iOs framework • How to handle deserialization on iOs
  50. droidcon EMEA - @marcoGomier Start little then go bigger •

    We have validated the process with “little” effort • Now we can go bigger and share more “features” For example the data layer → write SQL once for all
  51. 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/
  52. droidcon EMEA Marco Gomiero Thank you! > Twitter: @marcoGomier >

    Github: prof18 > Website: marcogomiero.com