Kotlin Multiplatform Project for Android and iOS: Getting Started (Part-2)

Deepak Sikka
8 min readMay 5, 2021

This Story is from the Kotlin- Series, now we will going to add some more points in Kotlin Multiplatform Mobile(KMM). If You have not Configure KMM plugin in your android Studio than you can refer my previous link :

In this tutorial, you’ll learn how to build and update an app for Android and iOS while only having to write the business logic once in Kotlin. More specifically, you’ll learn how to:

  • Integrate KMM into an existing project.
  • Set up the common module.
  • Fetch data from a network.
  • Save data.

Multiplatform

Kotlin Multiplatform supports more than just Android and iOS. It also supports JavaScript, Windows and other native targets. You’ll focus on the mobile aspect, or Kotlin Multiplatform Mobile (KMM).

Common Code

KMM allows shared code in Android and iOS. On Android, it does this by using a shared module with Gradle. On iOS, you can import the shared module as a framework that you can access from Swift code.

If you think it’s easy to understand how Android uses common code, you’re right. It’s a simple include in settings.gradle. However, as mentioned earlier, for iOS it’s different, as the code is compiled to native code. You’ll go through the steps of creating this framework later.

Root project

The root project is a Gradle project that holds the shared module and the Android application as its subprojects. They are linked together via the Gradle multi-project mechanism.

// settings.gradle.kts
include(":shared")
include(":androidApp")

This is a basic structure of a KMM project:

The root project doesn’t hold source code. You can use it to store global configuration in its build.gradle(.kts) or gradle.properties, for example, add repositories or define global configuration variables.For more complex projects, you can add more modules into the root project by creating them in the IDE and linking via include declarations in the Gradle settings.

Shared module

Shared module contains the core application logic used in both target platforms: classes, functions, and so on. This is a Kotlin Multiplatform module that compiles into an Android library and an iOS framework. It uses Gradle with the Kotlin Multiplatform plugin applied and has targets for Android and iOS.

plugins {
kotlin("multiplatform") version "1.4.30"
// ..
}

kotlin {
android()
ios()
}

Source sets

The shared module contains the code that is common for Android and iOS applications. However, to implement the same logic on Android and iOS, you sometimes need to write two platform-specific versions of it. To handle such cases, Kotlin offers the expect/actual mechanism. The source code of the shared module is organized in three source sets accordingly:

  • commonMain stores the code that works on both platforms, including the expect declarations
  • androidMain stores Android-specific parts, including actual implementations
  • iosMain stores iOS-specific parts, including actual implementations

Each source set has its own dependencies. Kotlin standard library is added automatically to all source sets, you don’t need to declare it in the build script.

kotlin {
sourceSets {
val commonMain by getting
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.2.0")
}
}
val iosMain by getting
// ...
}
}

Use them to store unit tests for common and platform-specific source sets accordingly. By default, they have dependencies on Kotlin test library, providing you with means for Kotlin unit testing: annotations, assertion functions and other. You can add dependencies on other test libraries you need.

kotlin {
sourceSets {
// ...
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val androidTest by getting
val iosTest by getting
}

}

Android library

The configuration of the Android library produced from the shared module is typical for Android projects. To learn about Android libraries creation, see Create an Android library in the Android developer documentation.

To produce the Android library, two more Gradle plugins are used in addition to Kotlin Multiplatform:

  • Android library
  • Kotlin Android extensions.
plugins {
// ...
id("com.android.library")
id("kotlin-android-extensions")
}

The configuration of Android library is stored in the android {} top-level block of the shared module’s build script.

android {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
}

iOS framework

For using in iOS applications, the shared module compiles into a framework — a kind of hierarchical directory with shared resources used on the Apple platforms. This framework connects to the Xcode project that builds into an iOS application.

The framework is produced via the Kotlin/Native compiler. The framework configuration is stored in the ios {} block of the build script within kotlin {}. It defines the output type framework and the string identifier baseName that is used to form the name of the output artifact. Its default value matches the Gradle module name. For a real project, it’s likely that you’ll need a more complex configuration of the framework production. For details, see Multiplatform documentation.

kotlin {
// ...
ios {
binaries {
framework {
baseName = "shared"
}
}
}
}

Additionally, there is a Gradle task that exposes the framework to the Xcode project from which the iOS application is built. It uses the configuration of the iOS application project to define the build mode (debug or release) and provide the appropriate framework version to the specified location.

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)
}

The task executes upon each build of the Xcode project to provide the latest version of the framework for the iOS application. For details, see iOS application.

tasks.getByName("build").dependsOn(packForXcode)

iOS application

The iOS application is produced from an Xcode project generated automatically by the Project Wizard. It resides in a separate directory within the root KMM project. This is a basic Xcode project configured to use the framework produced from the shared module.

To make the declarations from the shared module available in the source code of the iOS application, the framework is added to the project on the General tab of the project settings.

For each build of the iOS application, the project obtains the latest version of the framework. To do this, it uses a Run Script build phase that executes the packForXcode Gradle task from the shared module.

Finally, another build phase Embed Framework takes the framework from the specified location and embeds it into the application build. This makes the shared code available in the iOS application.

In other aspects, the Xcode part of a KMM project is a typical iOS application project.

Fetching Data From the Network in Common Code

To fetch data, you need a way to use networking from common code. Ktor is a multiplatform library that allows performing networking on common code. In addition, to parse/encapsulate the data, you can use a serialization library. Kotlin serialization is a multiplatform library that will allow this from common code.

To start, add the dependencies. Go back to Android Studio, open shared/build.gradle.kts and add the following lines of code below the import but above plugins:

val ktorVersion = "1.5.0"
val coroutineVersion = "1.4.2"

You’ve defined the library versions to be used. Now, inside plugins at the bottom add:

kotlin("plugin.serialization")

Next, replace the code inside of sourceSets which is inside of kotlin with:

// 1
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation(
"org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("io.ktor:ktor-client-core:$ktorVersion")
}
}
// 2
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.2.0")
implementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
}
// 3
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-ios:$ktorVersion")
}
}

Here’s what you’re doing in the code above:

  1. Adding the dependencies to the common module.
  2. Declaring the dependencies to the Android module.
  3. Filling the dependencies into the iOS module.

Next, you need to add the data models. Create a file named Grain.kt under shared/src/commonMain/kotlin/com.raywenderlich.android.multigrain.shared. Add the following lines inside this file:

package com.raywenderlich.android.multigrain.shared

import kotlinx.serialization.Serializable

@Serializable
data class Grain(
val id: Int,
val name: String,
val url: String?
)

This defines the data model for each grain entry.

Create another file inside the same folder, but this time, name it GrainList.kt. Update the file with the following:

package com.raywenderlich.android.multigrain.shared

import kotlinx.serialization.Serializable

@Serializable
data class GrainList(
val entries: List<Grain>
)

This defines an array of grains for parsing later.

Since now you have the data models, you can start writing the class for fetching data.

In the same folder/package, create another file called GrainApi.kt. Replace the contents of the file with:

package com.raywenderlich.android.multigrain.shared

// 1
import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json

// 2
class GrainApi() {
// 3
// 3
private val apiUrl =
"https://gist.githubusercontent.com/jblorenzo/" +
"f8b2777c217e6a77694d74e44ed6b66b/raw/" +
"0dc3e572a44b7fef0d611da32c74b187b189664a/gistfile1.txt"

// 4
fun getGrainList(
success: (List<Grain>) -> Unit, failure: (Throwable?) -> Unit) {
// 5
GlobalScope.launch(ApplicationDispatcher) {
try {
val url = apiUrl
// 6
val json = HttpClient().get<String>(url)
// 7
Json.decodeFromString(GrainList.serializer(), json)
.entries
.also(success)
} catch (ex: Exception) {
failure(ex)
}
}
}

// 8
fun getImage(
url: String, success: (Image?) -> Unit, failure: (Throwable?) -> Unit) {
GlobalScope.launch(ApplicationDispatcher) {
try {
// 9
HttpClient().get<ByteArray>(url)
.toNativeImage()
.also(success)
} catch (ex: Exception) {
failure(ex)
}
}
}
}

For better refernces please follow below link:

Thanks for reading this article. Be sure to clap to recommend this article if you found it helpful. It means a lot to me.

--

--

Deepak Sikka

Senior Android Developer. Working on technology Like Java,Kotlin, JavaScript.Exploring Block Chain technology in simple words.