Share
Embed
Adopting Kotlin Multiplatform in Brownfield Applications - DCIT22
Presented at Droidcon Italy 2022
How can your team start sharing code with Kotlin Multiplatform? This is a simple question for a complex, and nuanced challenge.
Over the past year, our team has invested into Kotlin Multiplatform for both our iOS and Android applications. In this talk, we'll discuss our lessons learned and highlight how to solve key challenges including:
- Getting organizational buy in
- Configuring a multi-artifact build
- Building a CI/CD pipeline
- Publishing .jar, .aar, and Swift Packages
- Integration into brownfield applications
- Optimizing developer experience
By the end of this talk, you should have a clear technical and product roadmap for integrating Kotlin Multiplatform into your apps.
Nate Ebel
October 07, 2022
Transcript
120 slides
-
Where are we going? • How we got organizational buy
in • How we solved key technical integration challenges • How we are using Kotlin Multiplatform today; and lessons learned -
Kotlin Multiplatform at Premise Blog series • Kotlin Multiplatform at
Premise • Kotlin Multiplatform Project Structure for Integration with Brown fi eld Applications • Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions -
About our team • 8 Android developers • 7 8
iOS developers • Android- fi rst feature development • Localized in 40 languages • Monthly active users in the hundreds of thousands -
Rationalizing analytics at Premise • Enumerate standard types of analytics
events • De fi ne rule patterns for each event type • Create shared documentation for Mobile team -
Why Kotlin Multiplatform? • Clear set of business rules to
codify • Want to share that logic across iOS, Android (maybe backend) • Don’t need/want shared UI • Have engineers well versed in Kotlin • Have engineers comfortable with non-trivial Gradle builds • Want low-risk, medium/low-e ff ort integration into existing apps • Have engineers willing to prototype, learn, and educate -
What next? How do we validate this KMP solution for
shared analytics? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Validating KMP in production • Ship a single Kotlin Multiplatform
String constant in production • Ensure no issues with development, CI/CD, or runtime in the wild -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Where will this new code live? Greenfield KMP Project iOS
Shared Android All projects live side by side in a single mono repo -
Where will this new code live? Brownfield iOS Project iOS
Brownfield Android Project Android Greenfield KMP Project Shared Existing projects are untouched Shared code exists in a new, separate repo -
Where will this new code live? Brownfield iOS Project iOS
Brownfield Android Project Android Greenfield KMP Project Shared Existing projects are untouched Shared code exists in a new, separate repo -
Greenfield KMP Project Shared How to structure this new KMP
project? iosMain androidMain commonMain Are these the right targets for our use case? -
Greenfield KMP Project Shared Are these the right targets for
our use case? iosMain androidMain commonMain -
Greenfield KMP Project Shared Are these the right targets for
our use case? iosMain commonMain jvmMain Chose JVM over Android to support integration with JVM- based backend services -
Configuring Build Targets // analytics/build.gradle.kts plugins { kotlin(“multiplatform”) } kotlin
{ jvm() iosX64() iosArm64() } -
Configuring Build Targets kotlin { jvm() iosX64() iosArm64() sourceSets {
val commonMain by getting val jvmMain by getting val iosX64Main by getting val iosArm64Main by getting } } -
Configuring Build Targets kotlin { jvm() iosX64() iosArm64() sourceSets {
val commonMain by getting val jvmMain by getting val iosX64Main by getting val iosArm64Main by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this) iosArm64Main.dependsOn(this) } } } -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Greenfield KMP Project Shared What if we want multiple multiplatform
artifacts? iosMain commonMain jvmMain Imagine the N+1 solution of additional multiplatform features -
Greenfield KMP Project What if we want multiple multiplatform artifacts?
iosMain commonMain jvmMain Imagine the N+1 solution of additional multiplatform features -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? iosMain commonMain jvmMain Imagine the N+1 solution of additional multiplatform features -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? Imagine the N+1 solution of additional multiplatform features -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? DTOs Networking Recommendations -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? DTOs Networking Recommendations Android -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? DTOs Networking Recommendations iOS -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? DTOs Networking Recommendations iOS -
Greenfield KMP Project Analytics What if we want multiple multiplatform
artifacts? DTOs Networking Recommendations iOS Shared -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption // shared/build.gradle.kts plugins { kotlin(“multiplatform”) } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption // shared/build.gradle.kts plugins { kotlin(“multiplatform”) } kotlin { val xcf = XCFramework(rootProject.name) } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption // shared/build.gradle.kts plugins { kotlin(“multiplatform”) } kotlin { val xcf = XCFramework(rootProject.name) listOf(iosX64(), iosArm64(), iosSimulatorArm64()) .forEach { target -> } } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption // shared/build.gradle.kts plugins { kotlin(“multiplatform”) } kotlin { val xcf = XCFramework(rootProject.name) listOf(iosX64(), iosArm64(), iosSimulatorArm64()) .forEach { target -> target.binaries.framework { xcf.add(this) export(project(“:analytics”)) export(project(“:network”)) transitiveExport = true } } } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption kotlin { val xcf = XCFramework(rootProject.name) listOf(iosX64(), iosArm64(), iosSimulatorArm64()) .forEach { target -> target.binaries.framework { xcf.add(this) export(project(“:analytics”)) export(project(“:network”)) transitiveExport = true } } } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption kotlin { val xcf = XCFramework(rootProject.name) listOf(iosX64(), iosArm64(), iosSimulatorArm64()) .forEach { target -> target.binaries.framework { xcf.add(this) export(project(“:analytics”)) export(project(“:network”)) transitiveExport = true } } } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption kotlin { . . . } -
Configuring a Composite Module Combine separate modules into 1 for
iOS consumption kotlin { . . . sourceSets { . . . iosMain by creating { dependencies { api(project(“:analytics”)) api(project(“:network”)) } } } } -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Building Output Artifacts • Android / JVM • .jar /
.aar • maven-publish plugin • iOS • XCFramework • Cocoapods • Swift Package Manager -
Building Android Output Artifacts / / analytics/build.gradle.kts plugins { kotlin(“multiplatform”)
id(“maven-publish”) id(“com.android.library”) } -
Building Android Output Artifacts / / analytics/build.gradle.kts plugins { kotlin(“multiplatform”)
id(“maven-publish”) id(“com.android.library”) } -
Building Android Output Artifacts // analytics/build.gradle.kts plugins { … }
kotlin { android { publishAllLibraryVariants() } } -
Building iOS Swift Package / / shared/build.gradle.kts plugins { kotlin(“multiplatform”)
id(“com.chromaticnoise.multiplatform-swiftpackage”) } -
Building iOS Swift Package // shared/build.gradle.kts plugins { . .
. } multiplatformSwiftPackage { swiftToolsVersion(“5.3”) targetPlatforms { iOS { v(“13”) } } packageName(rootProject.name) distributionMode { local() } } -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Local Deployment for Android Consuming from mavenLocal() ./gradlew build publishToMavenLocal
~/.m2 build.gradle allProjects { repositories { mavenLocal() } } Android -
Local Deployment for iOS Consuming local Swift Package ./gradlew build
createSwiftPackage shared/output/swiftpackage iOS 1) add package dependency 2) add local 3) select shared/output/swiftpackage Xcode -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
How do we ensure low cost deployment? Building a CI/CD
pipeline • How to build the project in CI? • How to deploy the .jar and .aar fi les to internal Artifactory server? • How to deploy the Swift Package? -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage -
Deploying Android Artifacts / / analytics/build.gradle.kts plugins { . .
. } / / hand-wavy pseudocode artifactory { setContextUrl(“<your server url>”) } -
Deploying Android Artifacts / / analytics/build.gradle.kts plugins { . .
. } / / hand-wavy pseudocode artifactory { setContextUrl(“<your server url>”) } -
Deploying Android Artifacts / / analytics/build.gradle.kts plugins { . .
. } / / hand-wavy pseudocode artifactory { setContextUrl(“<your server url>”) repository { setProperty(“repoKey”, “<repo>”) setProperty(“username”, “<username>”) setProperty(“password”, “<password>”) setProperty(“maven”, true) } } -
Deploying Android Artifacts / / hand-wavy pseudocode artifactory { setContextUrl(“<your
server url>”) repository { setProperty(“repoKey”, “<repo>”) setProperty(“username”, “<username>”) setProperty(“password”, “<password>”) setProperty(“maven”, true) } } -
Deploying Android Artifacts / / hand-wavy pseudocode artifactory { setContextUrl(“<your
server url>”) repository { setProperty(“repoKey”, “<repo>”) setProperty(“username”, “<username>”) setProperty(“password”, “<password>”) setProperty(“maven”, true) } } -
Deploying Android Artifacts / / hand-wavy pseudocode artifactory { setContextUrl(“<your
server url>”) repository { setProperty(“repoKey”, “<repo>”) setProperty(“username”, “<username>”) setProperty(“password”, “<password>”) setProperty(“maven”, true) } defaults { invokeMethod(“publications”, arrayOf(“jvm”, “kotlinmultiplatform”)) } } -
Consuming Android Artifact / / build.gradle.kts repositories { maven(url =
“<your maven artifact server>”) } / / app/build.gradle.kts dependencies { implementation(“com.yourpackage.analytics:1.0.0”) } -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage ./gradlew publishToArtifactory -
Deploying Swift Packages shared.xcframework Package.swift premise/mobile-shared-swift-package .gitignore import PackageDescription let
package = Package( name: “shared”, platforms: [ .iOS(.v13)], products: [ .library( name: “shared”, targets: [“shared”] ), ], targets: [ .binaryTarget( name: “shared”, path: “./shared.xcframework” ) ] ) / / Package.swift -
Consuming Swift Packages Xcode 1) Select Add framework or library
2) Select Add Package Dependency 3) Enter package url into search bar 4) Authenticate with GitHub 5) Select Add Package -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage ./gradlew publishToArtifactory -
Building the Project with GitHub Actions checkout setup-java setup-xcode ./gradlew
build ./gradlew createSwiftPackage ./gradlew publishToArtifactory clone mobile-shared-swift-package commit, push & tag swiftpackage -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Primary questions How do we build and scale a KMP
solution? • Where will this new code be stored? • How to structure a non-monorepo KMP project? • What multiplatform targets to choose? • How to distribute a multi-module artifact to iOS? • How to build output artifacts? • How to build, deploy, test locally? • How to ensure low cost deployment? • How to integrate into our existing iOS and Android projects? -
Challenges Build Times Swift Package Generation API Usability Shared Ownership
Swift Package Sync Time -
• Building XCFramework takes a long time • Build only
what you need • Skip iOS targets if developing/iterating on Android • Reduce iOS targets if developing/iterating on speci fi c iOS target Build Times -
• multiplatform-swiftpackage hasn’t been updated with M1 support • Community
forks available • Rolled our own custom Gradle task • Less fully-featured • Very focused on our exact needs Swift Package Generation -
• Syncing our Swift Package in XCode can take 30+
min • Publishing XCFrameworks to GitHub increases download size of repo • XCode doesn’t support shallow clone of the Swift Package repo • Update Swift Package to use a remote URL for framework download • Keeps large fi les out of the repo • Keeps XCode package sync fast Swift Package Sync Time -
• Kotlin features don’t always translate well to Objective-C •
Enums, Sealed Classes, suspend functions, default parameters • Write or consume wrappers to improve api usability • Collaborate with iOS team to fi nd opportunities for improvement • Community work into improved Swift API generation API Usability -
• Majority of development, maintenance, and understanding consolidated in 1-2
members of the team • Encouraging shared ownership • Lunch-n-learns to build shared understanding • All Mobile developers can build both iOS and Android projects Shared Ownership -
Collaborate From the Beginning • Get feedback from both iOS
and Android teams early, and often • Validate new patterns across both platforms • Regularly build both projects to understand developer experience • Onboard new developers to shared project early on • Encourage contributions from all team members -
Tips For Adopting Kotlin Multiplatform • “Let the problem de
fi ne the solution” • “Evaluate both technical and human factors” • “Start small” • “Think like a library developer” • “Collaborate from the beginning” Tip #1 Tip #2 Tip #3 Tip #4 Tip #5 -
Kotlin Multiplatform at Premise Blog series • Kotlin Multiplatform at
Premise • Kotlin Multiplatform Project Structure for Integration with Brown fi eld Applications • Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions