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.

9da5d5cc4b6a9f28058152e28364b02a?s=128

Marco Gomiero

October 08, 2020
Tweet

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 Story #1

  4. droidcon EMEA - @marcoGomier Alice Mobile Developer

  5. droidcon EMEA - @marcoGomier Alice Mobile Developer data class NewsDTO(

    val id: Long, val author: String )
  6. 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
  7. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

  8. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

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

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

    ""...
  11. droidcon EMEA - @marcoGomier Alice Mobile Developer Bob Backend Developer

    Oh, you’re right! Sorry! I’ve changed the value last day!
  12. droidcon EMEA @marcoGomier Story #2

  13. droidcon EMEA - @marcoGomier Alice Mobile Developer Charlie Product Manager

  14. droidcon EMEA - @marcoGomier Alice Mobile Developer Charlie Product Manager

    Can you now implement this logic on iOs as well? Sure!
  15. droidcon EMEA - @marcoGomier Alice Mobile Developer Charlie Product Manager

    ""...
  16. droidcon EMEA - @marcoGomier Alice Mobile Developer Charlie Product Manager

    I’m just converting Kotlin into Swift!
  17. droidcon EMEA - @marcoGomier

  18. droidcon EMEA - @marcoGomier

  19. droidcon EMEA - @marcoGomier Execution speed

  20. droidcon EMEA @marcoGomier How to tackle

  21. droidcon EMEA - @marcoGomier How to tackle • Cross Platform

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

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

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

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

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

    all platforms Kotlin Multiplatform
  27. 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
  28. droidcon EMEA @marcoGomier How to approach

  29. droidcon EMEA - @marcoGomier In most of the cases you

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

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

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

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

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

    Utility methods, aka `object Utils {}` Where to start?
  36. droidcon EMEA @marcoGomier Let’s start building

  37. droidcon EMEA - @marcoGomier

  38. droidcon EMEA - @marcoGomier https://github.com/prof18/shared-hn-android-ios-backend

  39. droidcon EMEA - @marcoGomier • Create a new project Let’s

    start building
  40. droidcon EMEA - @marcoGomier Create a new project

  41. droidcon EMEA - @marcoGomier • Create a new project •

    Add the JVM target [if needed] Let’s start building
  42. droidcon EMEA - @marcoGomier Add the JVM target [if needed]

  43. 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
  44. 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
  45. 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
  46. 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 )
  47. droidcon EMEA - @marcoGomier Publish the artifacts ./gradlew publish

  48. droidcon EMEA - @marcoGomier Publish the artifacts ./gradlew publish ./gradlew

    publishToMavenLocal
  49. 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
  50. droidcon EMEA - @marcoGomier Android implementation("com.prof18.hn.foundation:hn-foundation-android:1.0.0")

  51. droidcon EMEA - @marcoGomier Android interface HNApiService { @GET("hn/topStories") suspend

    fun getTopStories(): List<NewsDTO> }
  52. droidcon EMEA - @marcoGomier ! Backend implementation("com.prof18.hn.foundation:hn-foundation-jvm:1.0.0")

  53. droidcon EMEA - @marcoGomier ! Backend routing { get("/hn/topStories") {

    call.respond(getTopStories()) } } fun getTopStories(): List<NewsDTO> { ""... }
  54. droidcon EMEA - @marcoGomier

  55. droidcon EMEA - @marcoGomier  And iOs?

  56. droidcon EMEA - @marcoGomier

  57. droidcon EMEA - @marcoGomier Framework

  58. 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
  59. 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)
  60. 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
  61. 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 } ]
  62. 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 } ]
  63. droidcon EMEA - @marcoGomier

  64. droidcon EMEA - @marcoGomier Fat Framework

  65. droidcon EMEA - @marcoGomier org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask"::class

  66. 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
  67. 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
  68. 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
  69. droidcon EMEA - @marcoGomier https://github.com/prof18/hn-foundation-cocoa/ CocoaPod Repository

  70. 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" "=> "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
  71. None
  72. https:"//guides.cocoapods.org/making/private-cocoapods.html

  73. droidcon EMEA - @marcoGomier iOs Project: Podfile # For develop

    releases: pod 'HNFoundation', :git "=> "git@github.com:prof18/hn-foundation-cocoa.git", :branch "=> 'develop'
  74. droidcon EMEA - @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'
  75. 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?
  76. 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())
  77. 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 } } } }
  78. 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?
  79. 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 } } } }
  80. droidcon EMEA - @marcoGomier

  81. droidcon EMEA - @marcoGomier

  82. droidcon EMEA - @marcoGomier How to handle deserialization?

  83. droidcon EMEA - @marcoGomier How to handle deserialization? • Probably

    you are using Alamofire for net handling
  84. 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
  85. 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
  86. droidcon EMEA - @marcoGomier

  87. droidcon EMEA - @marcoGomier https:"//developer.apple.com/documentation/foundation/ archives_and_serialization/encoding_and_decoding_custom_types

  88. droidcon EMEA - @marcoGomier

  89. droidcon EMEA - @marcoGomier https:"//github.com/tristanhimmelman/ObjectMapper

  90. droidcon EMEA - @marcoGomier NewsDTODecodable.swift https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-ios-client/HN%20Client/HN%20Client/data/model/NewDTODecodable.swift class NewsDTODecodable: NewsDTO, Decodable

    { "// ".. }
  91. 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
  92. 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( ".. ) { ".. } }
  93. iOs Net Call https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-ios-client/HN%20Client/HN%20Client/ui/MainViewModel.swift let request = AF.request("http:"//192.168.0.119:8080/hn/topStories") request.responseDecodable(of: [NewsDTODecodable].self)

    { (response) in guard let listResponse = response.value else { print("something wrong") } "// ".. }
  94. Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared <object>@c2ddb8

    from other thread
  95. droidcon EMEA - @marcoGomier

  96. droidcon EMEA - @marcoGomier https:"//www.youtube.com/watch?v=oxQ6e1VeH4M

  97. droidcon EMEA - @marcoGomier

  98. droidcon EMEA - @marcoGomier https:"//dev.to/kpgalligan/series/3739

  99. droidcon EMEA - @marcoGomier

  100. droidcon EMEA - @marcoGomier Freeze the object to share it

    between threads
  101. droidcon EMEA - @marcoGomier https://github.com/touchlab/Stately

  102. droidcon EMEA - @marcoGomier BaseDTO.kt https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-foundation/src/commonMain/kotlin/com/prof18/hn/dto/BaseDTO.kt open class BaseDTO {

    fun makeFrozen() { freeze() } }
  103. 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
  104. droidcon EMEA - @marcoGomier

  105. droidcon EMEA @marcoGomier Conclusions

  106. 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
  107. droidcon EMEA - @marcoGomier Start little

  108. droidcon EMEA - @marcoGomier Start little Go bigger then

  109. 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
  110. 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/
  111. droidcon EMEA Marco Gomiero Thank you! > Twitter: @marcoGomier >

    Github: prof18 > Website: marcogomiero.com