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 Italy

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

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

November 11, 2021
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

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

    shared code between Android, iOS and the Backend
  2. droidcon Italy - @marcoGomier Marco Gomiero 👨💻 Android Engineer @

    TIER 🛴 🇩🇪 🇮🇹 
 Google Developer Expert for Kotlin 
 🍻 Co-Lead @ GDG Venezia > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com
  3. droidcon Italy - @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 Italy - @marcoGomier 👩💻 Alice Mobile Developer 👨💻 Bob

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

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

    Backend Developer Oh, you’re right! Sorry! I’ve changed the value last day!
  7. droidcon Italy - @marcoGomier 👩💻 Alice Mobile Developer 🧑💻 Charlie

    Product Manager Can you now implement this logic on iOs as well? Sure!
  8. droidcon Italy - @marcoGomier 👩💻 Alice Mobile Developer 🧑💻 Charlie

    Product Manager I’m just converting Kotlin into Swift!
  9. droidcon Italy - @marcoGomier Cross Platform solutions (React Native, Flutter,

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

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

    solutions (React Native, Flutter, .. ) • Kotlin Multiplatform
  12. droidcon Italy - @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
  13. droidcon Italy - @marcoGomier In most of the cases you

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

    a big rewrite Photo by Ashkan Forouzani on Unsplash
  15. droidcon Italy - @marcoGomier droidcon Italy - @marcoGomier . └──

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

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

    Utility methods, aka `object Utils {}` • Analytics • . . . Where to start?
  19. droidcon Italy - @marcoGomier Where to start? • DTOs •

    Common Models • Utility methods, aka `object Utils {}` • Analytics • . . .
  20. droidcon Italy - @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
  21. droidcon Italy - @marcoGomier • Create a new project •

    Add the JVM target [if needed] Let’s start building
  22. droidcon Italy - @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 Italy - @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 Italy - @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 Italy - @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 Italy - @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 Italy - @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)
  28. droidcon Italy - @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
  29. droidcon Italy - @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 } ]
  30. droidcon Italy - @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 } ]
  31. droidcon Italy - @marcoGomier XCFramework Official Support from Kotlin 1.5.30

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

    / / www.marcogomiero.com/posts/2021/build-xcframework-kmp/
  34. droidcon Italy - @marcoGomier . ├── build ├── XCFrameworks ├──

    debug │ └── LibraryName.xcframework └── release └── LibraryName.xcframework XCFramework
  35. droidcon Italy - @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" = > "[email protected]" } s.vendored_frameworks = 'HNFoundation.xcframework' s.source = { :git => "[email protected]:prof18/hn-foundation-cocoa-xcframework", :tag => " #{ s.version}" } s.exclude_files = "Classes/Exclude" end
  36. droidcon Italy - @marcoGomier iOs Project: Podfile # For develop

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

    releases: pod 'HNFoundation', :git => “[email protected]:prof18/hn-foundation-cocoa-xcframework.git", :branch = > 'develop' # For stable releases pod 'HNFoundation', :git => “[email protected]:prof18/hn-foundation-cocoa-xcframework.git", :tag => ‘2.0.0'
  38. https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/build.gradle.kts#L110 Publish Dev Framework Task 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") } .. ..
  39. droidcon Italy - @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())
  40. droidcon Italy - @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")
  41. droidcon Italy - @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 } }
  42. 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") } .. ..
  43. droidcon Italy - @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) {
  44. droidcon Italy - @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 } } } }
  45. droidcon Italy - @marcoGomier How to handle deserialization? • Probably

    you are using Alamofire for net handling • [De]Serialization probable handled with Codable 
 (or other solutions)
  46. droidcon Italy - @marcoGomier How to handle deserialization? • Probably

    you are using Alamofire for net handling • [De]Serialization probable handled with Codable 
 (or other solutions) • Need to comply to the Decodable protocol • Not yet available on Kotlin/Native: KT-48081
  47. droidcon Italy - @marcoGomier How to handle deserialization? • Need

    to comply to the Decodable protocol • Use kotlinx.serialization • Write a custom serializer for Alamofire
  48. droidcon Italy - @marcoGomier import Foundation import Alamofire import HNFoundation

    struct CustomSerializer<T: BaseDTO> : ResponseSerializer { func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { guard error == nil else { throw error! } guard let data = data, !data.isEmpty else { guard emptyResponseAllowed(forRequest: request, response: response) else { throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) } guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) } return emptyValue CustomSerializer.swift https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-ios-client/HN%20Client/HN%20Client/data/CustomSerializer.swift
  49. droidcon Italy - @marcoGomier https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-ios-client/HN%20Client/HN%20Client/data/CustomSerializer.swift throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) }

    return emptyValue } do { let jsonString = try StringResponseSerializer().serialize(request: request, response: response, data: data, error: error) let deserializedObject = try T().deserialize(jsonString: jsonString) as! T deserializedObject.makeFrozen() return deserializedObject } catch { throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) } } }
  50. droidcon Italy - @marcoGomier BaseDTO.kt https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/src/commonMain/kotlin/com/prof18/hn/dto/BaseDTO.kt abstract class BaseDTO {

    @Throws(Exception :: class) abstract fun deserialize(jsonString: String): BaseDTO protected val json = Json { ignoreUnknownKeys = true } fun makeFrozen() { freeze() } }
  51. droidcon Italy - @marcoGomier NewsDTO.kt https://github.com/prof18/shared-hn-android-ios-backend/blob/main/hn-foundation/src/commonMain/kotlin/com/prof18/hn/dto/NewsDTO.kt @Serializable data class NewsDTO(

    val author: String, val id: Long, val score: Int, val timestamp: Long, val title: String, val type: String, val url: String ) : BaseDTO() { override fun deserialize(jsonString: String): NewsDTO { val newsDTO: NewsDTO = json.decodeFromString(jsonString) newsDTO.makeFrozen() return newsDTO } }
  52. droidcon Italy - @marcoGomier How to handle deserialization? https: /

    / www.marcogomiero.com/posts/2020/kotlin-serialization-alamofire/
  53. droidcon Italy - @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
  54. droidcon Italy - @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
  55. 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/
  56. droidcon Italy Marco Gomiero Thank you! > Twitter: @marcoGomier 


    > Github: prof18 
 > Website: marcogomiero.com