Estimated time: about 30 minutes
TL;DR
commonMain— Kotlin code shared across all platformsexpect— declared in the common module;actual— implemented per platformjvmMain,jsMain— platform-specific code- Reuse business logic written once on JVM (backend/Android) and JS (frontend)
Target audience: Intermediate or above Kotlin developers Prerequisites: Environment Setup, basic Kotlin syntax
Use Kotlin Multiplatform (KMP) to share business logic across multiple platforms. We’ll write a date formatter as a common module and implement it for both JVM and JS in a mini project.
Step 1 — Project Structure#
In a KMP project, code is organized into source sets.
shared/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/ # Code shared across all platforms
├── commonTest/kotlin/ # Common tests
├── jvmMain/kotlin/ # JVM-only implementation
├── jvmTest/kotlin/
├── jsMain/kotlin/ # JS-only implementation
└── jsTest/kotlin/graph TD
A["commonMain<br>(shared business logic)"] --> B["jvmMain<br>(Android / Server)"]
A --> C["jsMain<br>(Browser / Node.js)"]
A --> D["nativeMain<br>(iOS / Desktop)"]Figure: KMP source set branching — common business logic in commonMain branches out to platform-specific code in jvmMain, jsMain, and nativeMain.
Step 2 — Project Creation#
In IntelliJ IDEA: File → New → Project → Kotlin Multiplatform → Library.
Or write build.gradle.kts directly.
// settings.gradle.kts
rootProject.name = "kmp-date-formatter"
include(":shared")// shared/build.gradle.kts
plugins {
kotlin("multiplatform") version "2.0.0"
}
group = "com.example"
version = "0.1.0"
kotlin {
jvm {
jvmToolchain(17)
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(IR) {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmTest by getting
val jsMain by getting
val jsTest by getting
}
}Step 3 — Define the Common Interface (expect)#
expect is a contract that says “this declaration must be implemented by each platform.”
// shared/src/commonMain/kotlin/com/example/datetime/DateFormatter.kt
package com.example.datetime
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
// expect declaration — each platform provides an implementation
expect class PlatformDateFormatter() {
fun formatDate(date: LocalDate): String
fun formatDateTime(dateTime: LocalDateTime): String
fun formatRelative(date: LocalDate): String // "3 days ago", "today", etc.
}
// Common logic — business logic that uses expect
class DateService(private val formatter: PlatformDateFormatter = PlatformDateFormatter()) {
fun describeDate(date: LocalDate): String {
val relative = formatter.formatRelative(date)
val formatted = formatter.formatDate(date)
return "$relative ($formatted)"
}
fun describeDateTime(dateTime: LocalDateTime): String {
return formatter.formatDateTime(dateTime)
}
}// shared/src/commonMain/kotlin/com/example/datetime/DateUtils.kt
package com.example.datetime
import kotlinx.datetime.*
// Platform-independent common utilities
fun LocalDate.isWeekend(): Boolean {
val day = this.dayOfWeek
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY
}
fun LocalDate.daysUntil(other: LocalDate): Long {
return (other.toEpochDays() - this.toEpochDays()).toLong()
}
fun LocalDate.nextWeekday(): LocalDate {
var next = this.plus(1, DateTimeUnit.DAY)
while (next.isWeekend()) {
next = next.plus(1, DateTimeUnit.DAY)
}
return next
}
data class DateRange(val start: LocalDate, val end: LocalDate) {
val durationDays: Long get() = start.daysUntil(end)
operator fun contains(date: LocalDate): Boolean = date >= start && date <= end
}Step 4 — JVM Implementation (actual)#
// shared/src/jvmMain/kotlin/com/example/datetime/DateFormatter.jvm.kt
package com.example.datetime
import kotlinx.datetime.*
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
actual class PlatformDateFormatter {
private val locale = Locale.ENGLISH
private val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
.withLocale(locale)
private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(locale)
actual fun formatDate(date: LocalDate): String {
val javaDate = java.time.LocalDate.of(date.year, date.monthNumber, date.dayOfMonth)
return javaDate.format(dateFormatter)
}
actual fun formatDateTime(dateTime: LocalDateTime): String {
val javaDateTime = java.time.LocalDateTime.of(
dateTime.year, dateTime.monthNumber, dateTime.dayOfMonth,
dateTime.hour, dateTime.minute, dateTime.second
)
return javaDateTime.format(dateTimeFormatter)
}
actual fun formatRelative(date: LocalDate): String {
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val diff = today.daysUntil(date)
return when {
diff == 0L -> "today"
diff == 1L -> "tomorrow"
diff == -1L -> "yesterday"
diff > 0 -> "in $diff days"
else -> "${-diff} days ago"
}
}
}Step 5 — JS Implementation (actual)#
// shared/src/jsMain/kotlin/com/example/datetime/DateFormatter.js.kt
package com.example.datetime
import kotlinx.datetime.*
actual class PlatformDateFormatter {
// In JS, use the Intl.DateTimeFormat API (accessed via kotlin-js)
actual fun formatDate(date: LocalDate): String {
// Format in English style
return "${date.dayOfMonth}/${date.monthNumber}/${date.year}"
}
actual fun formatDateTime(dateTime: LocalDateTime): String {
return "${formatDate(dateTime.date)} " +
"${dateTime.hour.toString().padStart(2, '0')}:" +
"${dateTime.minute.toString().padStart(2, '0')}"
}
actual fun formatRelative(date: LocalDate): String {
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val diff = today.daysUntil(date)
return when {
diff == 0L -> "today"
diff == 1L -> "tomorrow"
diff == -1L -> "yesterday"
diff > 0 -> "in $diff days"
else -> "${-diff} days ago"
}
}
}Step 6 — Common Tests#
Common tests run on every platform.
// shared/src/commonTest/kotlin/com/example/datetime/DateUtilsTest.kt
package com.example.datetime
import kotlinx.datetime.LocalDate
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class DateUtilsTest {
@Test
fun `Saturday is weekend`() {
val saturday = LocalDate(2026, 5, 16) // Saturday
assertTrue(saturday.isWeekend())
}
@Test
fun `Monday is not weekend`() {
val monday = LocalDate(2026, 5, 11) // Monday
assertFalse(monday.isWeekend())
}
@Test
fun `verify date range membership`() {
val range = DateRange(
start = LocalDate(2026, 5, 1),
end = LocalDate(2026, 5, 31)
)
assertTrue(LocalDate(2026, 5, 15) in range)
assertFalse(LocalDate(2026, 6, 1) in range)
}
@Test
fun `daysUntil calculation`() {
val from = LocalDate(2026, 5, 1)
val to = LocalDate(2026, 5, 10)
assertEquals(9L, from.daysUntil(to))
}
@Test
fun `nextWeekday skips weekends`() {
val friday = LocalDate(2026, 5, 15) // Friday
val nextWorkday = friday.nextWeekday()
assertEquals(LocalDate(2026, 5, 18), nextWorkday) // Monday
}
}Step 7 — JVM Main (Usage Example)#
// shared/src/jvmMain/kotlin/com/example/Main.kt
package com.example
import com.example.datetime.*
import kotlinx.datetime.*
fun main() {
val formatter = PlatformDateFormatter()
val service = DateService(formatter)
// Format dates
val today = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault()).date
println("Today: ${formatter.formatDate(today)}")
// e.g., Today: May 13, 2026
// Relative dates
val nextWeek = today.plus(7, kotlinx.datetime.DateTimeUnit.DAY)
println("Next week: ${service.describeDate(nextWeek)}")
// e.g., Next week: in 7 days (May 20, 2026)
// Weekend check
println("Is today a weekend: ${today.isWeekend()}")
// Date range
val projectPeriod = DateRange(
start = today,
end = today.plus(30, kotlinx.datetime.DateTimeUnit.DAY)
)
println("Project duration: ${projectPeriod.durationDays} days")
}Step 8 — Build and Test#
# Run common tests (JVM + JS)
./gradlew :shared:allTests
# JVM only
./gradlew :shared:jvmTest
# JS only
./gradlew :shared:jsTest
# Run on JVM
./gradlew :shared:jvmRunBuild artifacts
# JAR for JVM
shared/build/libs/shared-jvm-0.1.0.jar
# JS bundle (Node.js / browser)
shared/build/js/packages/kmp-date-formatter-shared/What expect/actual supports
expect/actualapplies not only to classes but also to:
- Top-level functions:
expect fun currentTimeMs(): Long- Objects:
expect object Platform- Type aliases:
expect class IOException- Annotations:
expect annotation class Serializable
When KMP fits well
- Validation rules and business rules — identical logic on backend and frontend
- Date/time handling, unit conversions, formatting rules
- API response parsing models (data classes)
- Algorithms and computation logic
UI, file systems, and lower-layer networking are usually a better fit for platform-specific code.
Key takeaways
commonMain— platform-independent shared code (business logic, models, utilities)expect— declared in the common module, implemented asactualper platformjvmMain,jsMain— can use platform-specific APIs- Common tests run on every platform simultaneously, ensuring consistency
- Use official multiplatform libraries like
kotlinx-datetimeto expand the scope of shared code
Writing Tests#
KMP common tests live in the commonTest source set, and the kotlin("test") dependency alone is enough to run the same tests on both JVM and JS. Confirm the dependencies already present in the commonTest source set of build.gradle.kts.
// shared/build.gradle.kts — commonTest source set
val commonTest by getting {
dependencies {
implementation(kotlin("test")) // Includes assertEquals, assertNotNull, etc.
}
}commonTest source set layout
shared/src/
├── commonMain/kotlin/com/example/datetime/
│ ├── DateFormatter.kt (expect declaration)
│ └── DateUtils.kt (common utilities)
└── commonTest/kotlin/com/example/datetime/
├── DateRangeTest.kt (common test — runs on both JVM and JS)
└── DateServiceTest.ktUsing kotlin.test basic assertions
kotlin.test provides assertEquals, assertNotNull, assertTrue, assertFailsWith, and more. You can use them directly in commonTest without platform-specific imports.
// shared/src/commonTest/kotlin/com/example/datetime/DateRangeTest.kt
package com.example.datetime
import kotlinx.datetime.LocalDate
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class DateRangeTest {
@Test
fun `DateRange durationDays returns days between two dates`() {
val range = DateRange(
start = LocalDate(2026, 5, 1),
end = LocalDate(2026, 5, 31)
)
assertEquals(30L, range.durationDays)
}
@Test
fun `contains checks date membership`() {
val range = DateRange(
start = LocalDate(2026, 5, 1),
end = LocalDate(2026, 5, 31)
)
assertTrue(LocalDate(2026, 5, 15) in range)
assertFalse(LocalDate(2026, 6, 1) in range)
assertFalse(LocalDate(2026, 4, 30) in range)
}
@Test
fun `nextWeekday returns the next weekday by skipping the weekend`() {
val friday = LocalDate(2026, 5, 15) // Friday
val expected = LocalDate(2026, 5, 18) // Following Monday
val actual = friday.nextWeekday()
assertEquals(expected, actual)
assertNotNull(actual)
}
}Common unit tests for DateService
DateService uses PlatformDateFormatter, which requires expect/actual implementations. To verify the pure common logic, target classes like DateRange.
// shared/src/commonTest/kotlin/com/example/datetime/DateUtilsExtTest.kt
package com.example.datetime
import kotlinx.datetime.LocalDate
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class DateUtilsExtTest {
@Test
fun `Saturday and Sunday are weekend`() {
val saturday = LocalDate(2026, 5, 16)
val sunday = LocalDate(2026, 5, 17)
assertTrue(saturday.isWeekend())
assertTrue(sunday.isWeekend())
}
@Test
fun `weekdays are not weekend`() {
val monday = LocalDate(2026, 5, 11)
val wednesday = LocalDate(2026, 5, 13)
assertFalse(monday.isWeekend())
assertFalse(wednesday.isWeekend())
}
@Test
fun `daysUntil returns negative values too`() {
val from = LocalDate(2026, 5, 10)
val to = LocalDate(2026, 5, 1)
assertEquals(-9L, from.daysUntil(to))
}
}Run the tests on both JVM and JS simultaneously.
# Run common tests across all platforms
./gradlew :shared:allTests
# JVM only
./gradlew :shared:jvmTest
# JS only
./gradlew :shared:jsTestMain kotlin.test assertions
Function Description assertEquals(expected, actual)Asserts two values are equal assertNotNull(value)Asserts the value is non-null assertTrue(condition)Asserts the condition is true assertFalse(condition)Asserts the condition is false assertFailsWith<T> { }Asserts that a specific exception is thrown
kotlin.testmaps to JUnit 5 on JVM and Mocha on JS, so no platform-specific setup is required.
Next Steps#
- Multiplatform Overview — KMP architecture and platform support status
- Environment Setup — Gradle Kotlin DSL deep dive