Creating and setting up Kotlin Multiplatform projects for Android and iOS.— Kotlin Multiplatform Shared UI Series
Welcome back to the Kotlin Multiplatform Shared UI series! In case you missed the previous articles, be sure to check out the link to our previous post where we explored the architecture. In this third installment, we will dive into the nitty-gritty details of creating our Android and iOS KMM project. We’ll cover everything from adding dependencies to creating modules and building the shared build.gradle.kts file. So, let’s roll up our sleeves and get started!
Creating the project.
To create and build the app, we will be using Android Studio. While you can download the IDE from the official website, I highly recommend using Jetbrains Toolbox instead. This tool makes it easy to manage your JetBrains tools by allowing you to install, update, and even roll back and downgrade your plugins and IDE with just a few clicks. Plus, you’ll be able to keep everything up-to-date automatically, ensuring you always have access to the latest features and improvements.
I use the Android Studio Canary release so that I can use the latest versions of Kotlin And Gradle Plugin. But if you use a Stable release, you may need to lower your versions to sync the project successfully.
- When the Android Studio IDE is ready, the next step is to create a new Kotlin Multiplatform App from the templates available.
- When creating your project, you can give it any name you like. For this tutorial, I’ll be naming it “Shared UI KMM App”. You’ll also need to choose a package name, and a save location before pressing the “Next” button. On the following screen, simply press “Next” again to proceed.
Once the initial project sync is complete, navigate to the “Project” view in the file explorer. I prefer this view as it allows for a better understanding of the file hierarchy.
You can find this project on Github:
https://github.com/charlee-dev/Shared_UI_KMM_App
Dependencies
For managing dependencies in our project, we will be using buildSrc. If you’re not familiar with buildSrc
, I highly recommend reading this article to get a better understanding of its benefits and how it works.
In the root of your project, create a buildSrc
directory and add the necessary files and directories, as shown in the image below. It's worth noting that there is a buildSrc
directory inside the main buildSrc
directory. This is intentional and allows us to share build configurations across all modules.
Contents of all files:
- root/buildSrc/system.properties
java.runtime.version=11
- root/buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
implementation(Kotlin.gradle)
implementation(Android.agp)
}
kotlin {
sourceSets.getByName("main").kotlin.srcDir("buildSrc/src/main/kotlin")
}
- root/buildSrc/.gitignore
.gradle
/build
- root/buildSrc/src/main/kotlin/kmm-component.gradle.kts
Note: Modules.USECASE is commented for now. We will uncomment it we put some data in it.
plugins {
id("kmm-multiplatform")
}
kotlin {
sourceSets {
named("commonMain") {
dependencies {
implementation(project(Modules.USECASE))
implementation(Ballast.core)
}
}
}
}
- root/buildSrc/src/main/kotlin/kmm-multiplatform.gradle.kts
Note: If you work on Mac with M1/M2 processor you need to have iosSimulatorArm64() in your kotlin targets. And your iosSimulatorArm64Main sourceSet need to depend on iosMain.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("multiplatform")
id("com.android.library")
}
version = KotlinConfig.version
kotlin {
android()
ios()
iosSimulatorArm64()
kotlin {
sourceSets {
val commonTest by getting
val commonMain by getting {
commonTest.dependsOn(this)
dependencies {
implementation(Kotlin.coroutinesCore)
implementation(Koin.core)
}
}
val androidTest by getting
val androidMain by getting {
androidTest.dependsOn(this)
}
val iosTest by getting
val iosSimulatorArm64Test by getting
val iosSimulatorArm64Main by getting
val iosMain by getting {
this.dependsOn(commonMain)
iosTest.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
}
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}
}
android {
compileSdk = AndroidConfig.compile
defaultConfig {
minSdk = AndroidConfig.min
}
}
- root/buildSrc/buildSrc/build.gradle.kts
repositories {
mavenCentral()
}
plugins {
`kotlin-dsl`
}
- root/buildSrc/buildSrc/.gitignore
.gradle
/build
- root/buildSrc/buildSrc/src/main/kotlin/Config.kt
import org.gradle.api.JavaVersion
object AndroidConfig {
const val id = "com.adrianwitaszak.shareduikmmapp"
const val min = 21
const val compile = 33
const val target = compile
const val versionCode = 1
const val versionName = "1.0"
}
object iOSConfig {
const val deploymentTarget = "11.0"
}
object KotlinConfig {
const val version = "0.0.1"
const val jvmTarget = "11"
val javaVersion = JavaVersion.VERSION_11
}
object SqlDelightConfig {
const val databaseName = "shareduikmmapp"
val packagename = "${location(Modules.DATA_LOCAL)}.cache"
}
- root/buildSrc/buildSrc/src/main/kotlin/Modules.kt
object Modules {
const val ANDROID = ":android"
const val IOS = ":iosApp"
const val BACKEND = ":backend"
const val SHARED_UI = ":sharedUi"
const val FEATURE_ROOT = ":feature:root"
const val FEATURE_ROUTER = ":feature:router"
const val FEATURE_LOGIN = ":feature:login"
const val FEATURE_BT_LIST = ":feature:bt:list"
const val FEATURE_BT_DETAIL = ":feature:bt:detail"
const val USECASE = ":usecase"
const val HARDWARE_BT = ":hardware:bt"
const val HARDWARE_LOCATION = ":hardware:location"
const val DATA_SDK = ":data:sdk"
const val DATA_REMOTE = ":data:remote"
const val DATA_LOCAL = ":data:local"
}
fun String.name() = drop(1).replace(":", ".")
fun location(moduleName: String) = AndroidConfig.id + moduleName.name()
- root/buildSrc/buildSrc/src/main/kotlin/Plugins.kt
object Plugins {
const val ANDROID = "android"
const val ANDROID_APP = "com.android.application"
const val ANDROID_LIBRARY = "com.android.library"
const val APOLLO = "com.apollographql.apollo3"
const val JETBRAINS_COMPOSE = "org.jetbrains.compose"
const val KOTLIN_ANDROID = "org.jetbrains.kotlin.android"
const val KOTLIN_MULTIPLATFORM = "multiplatform"
const val KOTLIN_NATIVE_COCAPODS = "native.cocoapods"
const val SHOPPE_MULTIPLATFORM = "shoppe-multiplatform"
const val SHOPPE_COMPONENT = "shoppe-component"
}
- root/buildSrc/buildSrc/src/main/kotlin/Dependencies.kt
object Version {
const val activity = "1.6.1"
const val agp = "7.4.1"
const val ballast = "2.3.0"
const val composeCompiler = "1.3.2"
const val coroutines = "1.6.4"
const val jetbrainsCompose = "1.2.2"
const val jetpackCompose = "1.4.0-alpha04"
const val junit = "4.13.2"
const val kermit = "1.1.3"
const val koin = "3.3.2"
const val kotlin = "1.7.20"
const val material = "1.4.0"
const val mockk = "1.12.4"
const val settings = "1.0.0"
}
object Android {
const val agp = "com.android.tools.build:gradle:${Version.agp}"
}
object Ballast {
const val core= "io.github.copper-leaf:ballast-core:${Version.ballast}"
const val savedState= "io.github.copper-leaf:ballast-saved-state:${Version.ballast}"
const val navigation= "io.github.copper-leaf:ballast-navigation:${Version.ballast}"
const val test = "io.github.copper-leaf:ballast-test:${Version.ballast}"
}
object JetpackCompose {
const val activity = "androidx.activity:activity-compose:${Version.activity}"
const val runtime = "androidx.compose.runtime:runtime:${Version.jetpackCompose}"
const val ui = "androidx.compose.ui:ui:${Version.jetpackCompose}"
const val foundationLayout =
"androidx.compose.foundation:foundation-layout:${Version.jetpackCompose}"
const val material = "androidx.compose.material:material:${Version.jetpackCompose}"
}
object Kotlin {
const val gradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Version.kotlin}"
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.coroutines}"
const val coroutinesSwing = "org.jetbrains.kotlinx:kotlinx-coroutines-swing:${Version.coroutines}"
}
object Koin {
const val core = "io.insert-koin:koin-core:${Version.koin}"
const val android = "io.insert-koin:koin-android:${Version.koin}"
}
object Settings {
const val common = "com.russhwolf:multiplatform-settings:${Version.settings}"
const val coroutines = "com.russhwolf:multiplatform-settings-coroutines:${Version.settings}"
}
object Shared {
const val kermit = "co.touchlab:kermit:${Version.kermit}"
}
object Test {
const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.coroutines}"
const val mockk = "io.mockk:mockk:${Version.mockk}"
const val junit = "junit:junit:${Version.junit}"
}
- root/build.gradle.kts
plugins {
id(Plugins.JETBRAINS_COMPOSE) version Version.jetbrainsCompose apply false
}
- root/settings.gradle.kts
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}
rootProject.name = "Shared_UI_KMM_App"
include(":androidApp")
include(":shared")
- root/gradle.properties.kts
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -Dkotlin.daemon.jvm.options\="-Xmx4g"
org.gradle.parallel=true
kotlin.code.style=official
android.useAndroidX=true
android.nonTransitiveRClass=true
xcodeproj=./iosApp
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.stability.nowarn=true
kotlin.native.binary.memoryModel=experimental
kotlin.native.cacheKind=none
kotlin.native.useEmbeddableCompilerJar=true
kotlin.mpp.enableCInteropCommonization=true
compose.desktop.verbose=true
org.jetbrains.compose.experimental.macos.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true
Now we are ready to sync the project. 😀
Creating modules
Now we have all the necessary dependencies, we can create placeholders for our modules. We won't be using shared module, so we can convert it to our first module, “sharedUI”.
- Comment all modules in settings.gradle.kts so we can rename shared module to root and relocate it. Sync the project.
//include(":androidApp")
//include(":shared")
- Create directories for all our new modules like this.
- You can delete the shared module. We won't need it anymore.
- replace root/androidApp/build.gradle.kts code with new configuration using our buildSrcs commented for now.
plugins {
id(Plugins.ANDROID_APP)
id(Plugins.KOTLIN_ANDROID)
}
version = KotlinConfig.version
android {
namespace = location(Modules.ANDROID)
compileSdk = AndroidConfig.compile
defaultConfig {
applicationId = location(Modules.ANDROID)
minSdk = AndroidConfig.min
targetSdk = AndroidConfig.target
versionCode = AndroidConfig.versionCode
versionName = AndroidConfig.versionName
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = KotlinConfig.javaVersion
targetCompatibility = KotlinConfig.javaVersion
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Version.composeCompiler
}
}
dependencies {
implementation(project(Modules.SHARED_UI))
implementation(Koin.android)
with(JetpackCompose) {
api(activity)
implementation(runtime)
implementation(ui)
implementation(foundationLayout)
implementation(material)
}
}
- root/settings.gradle.kts replace commented modules with new ones
include(
":androidApp",
)
include(
":sharedUi",
)
include(
":feature:root",
":feature:router",
":feature:login",
":feature:bt:list",
":feature:bt:detail",
)
include(
":usecase",
)
include(
":hardware:bt",
":hardware:location",
)
include(
":data:sdk",
":data:local",
":data:remote",
)
To successfully sync the project, we will add build.gradle.kts, .gitignore and src with directories to every created module.
Every module should look like this:
In all feature modules, you need to change the name of the last directory to match the module name.
For example, creating directories for FEATURE_LOGIN with have directories “src/com/your_name/your_project_name/features/login”
- .gitignore contains only
/build
- build.gradle.kts. Replace Modules.FEATURE_ROOT with your current module name. Create this gradle file in all “Feature” modules.
plugins {
id(Plugins.KMM_COMPONENT)
}
android {
namespace = location(Modules.FEATURE_ROOT)
}
- In Usecase, Hardware, SharedUi and Data modles Gradle file will be slightly different because it will use KMM_MULTIPLATFORM Gradle configuration we created earlier. Update Modules.USECASE name.
plugins {
id(Plugins.KMM_MULTIPLATFORM)
}
android {
namespace = location(Modules.USECASE)
}
Now we are ready to sync the project. 😀
In conclusion, creating a Kotlin Multiplatform project for Android and iOS can be a complex process, but with the right tools and guidance, it’s definitely achievable. In this blog post, we covered the steps to set up a new project, manage dependencies using buildSrc, and create modules for shared code. We hope this information was helpful in getting you started on your Kotlin Multiplatform journey. Stay tuned for more articles in the Shared UI KMM series!
Navigation:
- Table of Contents
- ⏮ — App Architecture — All layers explained
- Current — Creating and setting up Kotlin Multiplatform projects for Android and iOS.
- ⏭ — Creating the Shared UI module.
Thank you for reading! I hope you found this post helpful. If you enjoyed it, please consider sharing it with your friends and colleagues. You can also follow me on Medium or Twitter to stay up-to-date on my latest posts. As always, I welcome your feedback and comments. Thank you again for your support!
Twitter — https://twitter.com/adrianwita
Github — https://github.com/charlee-dev