Kotlin Multiplatform Basics

Kotlin Intermediate — Kotlin Multiplatform Basics
Kotlin Intermediate
Course 2 · Chapter 8 · Kotlin Multiplatform Basics

🌍 Kotlin Multiplatform Basics

The final chapter of this course is the payoff for a language built without primitives, with erasure that's a JVM-specific concern rather than a language one: Kotlin Multiplatform (KMP) lets one Kotlin codebase target the JVM/Android, iOS, JavaScript, and native desktop from shared source. This chapter covers what KMP actually is, project structure, and the expect/actual pattern that makes platform-specific code possible within a shared codebase.

🧩 What Kotlin Multiplatform Actually Shares

KMP's core idea: write business logic, data models, and networking once, in plain Kotlin, and compile that same code to run natively on every target platform — not through a bridge or interpreter, but as genuinely native code on each platform (JVM bytecode for Android, native binaries for iOS via Kotlin/Native, JavaScript for web).

Typically Shared

Data models, business logic, ViewModels, networking code, local database access, validation rules — anything that's pure logic without a platform-specific UI dependency.

Typically Platform-Specific

The UI layer — Jetpack Compose on Android, SwiftUI on iOS — stays native to each platform in the traditional KMP approach, so each platform still looks and feels genuinely native.

⚠ Not the Same Approach as React Native or Flutter

React Native and Flutter share the UI layer itself across platforms (rendering through their own cross-platform UI toolkit). Traditional KMP deliberately does the opposite — share the logic, keep the UI 100% native per platform. (Compose Multiplatform, a newer, separate JetBrains project built on KMP, does extend sharing to the UI layer too — worth knowing it exists, but it's a distinct, additional layer on top of what this chapter covers.)

📁 Project Structure — Source Sets

A KMP project splits code into source sets — folders that each target a different scope of platforms:

project/ ├── commonMain/ # shared Kotlin code — compiles for EVERY target │ └── kotlin/ │ └── Repository.kt ├── androidMain/ # Android-only code │ └── kotlin/ │ └── PlatformInfo.kt ├── iosMain/ # iOS-only code │ └── kotlin/ │ └── PlatformInfo.kt └── jvmMain/ # desktop JVM-only code (if targeted) └── kotlin/ └── PlatformInfo.kt

commonMain is where the bulk of shared logic lives, and it's compiled separately for each actual target platform — the same source file produces genuinely different compiled output per platform (JVM bytecode, a native iOS binary, JS), even though it's written once.

🔀 expect / actual — Platform-Specific Implementations

Shared code in commonMain sometimes needs something only a specific platform can actually provide (a device ID, a platform name, a native API). expect declares the shape of that thing in shared code, without an implementation; each platform then supplies its own actual:

// commonMain/kotlin/Platform.kt — declares WHAT exists, not HOW expect fun platformName(): String // commonMain — this can use platformName() without knowing which platform it's on fun greet(): String = "Hello from ${platformName()}!"
// androidMain/kotlin/Platform.kt actual fun platformName(): String = "Android" // iosMain/kotlin/Platform.kt actual fun platformName(): String = "iOS"

greet(), written once in commonMain, produces "Hello from Android!" when compiled for Android and "Hello from iOS!" when compiled for iOS — the shared code calls platformName() without ever knowing (or needing to know) which platform's actual will end up wired in. Each platform's compiler substitutes its own actual for every expect declaration at compile time.

expect/actual Works on More Than Functions

Classes, properties, and objects can all be declared expect in common code with an actual per platform — not just top-level functions. A common pattern: an expect class DatabaseDriver with a platform-specific SQLite driver as each actual.

Every Platform Must Supply an actual

If a target platform is missing an actual for some expect declaration, the build fails for that platform specifically — the compiler enforces that every declared expectation is genuinely met everywhere it's compiled.

📦 Where Third-Party Libraries Fit In

Networking, serialization, and database libraries increasingly ship KMP-aware versions (Ktor for networking, kotlinx.serialization for JSON, SQLDelight for databases) — these already handle their own expect/actual internally, so shared code calling them doesn't need to write any platform-specific code itself. Reaching for an expect/actual pair by hand is really a last resort, for the genuinely platform-specific slice of an app that no shared library already covers.

Cross-Platform Approaches, Compared

ApproachWhat's SharedUI
Kotlin MultiplatformBusiness logic, data, networkingNative per platform (Compose / SwiftUI)
Compose MultiplatformLogic + UIShared Compose UI across platforms
React NativeLogic + UIShared UI, rendered via native components
FlutterLogic + UIShared UI, rendered via Flutter's own engine

💻 Coding Challenges

Challenge 1: A Basic expect/actual Pair

Write an expect fun currentTimestamp(): Long in "commonMain" (write it as a single file, with a comment marking where it belongs), then two actual implementations in comments labeled "androidMain" and "jvmMain" — both can call System.currentTimeMillis(), but write them as if in separate files/platforms to show the pattern. Add a shared function fun describeTimestamp(): String that uses currentTimestamp().

Goal: Practice the expect/actual declaration pattern, even without a real multi-target build.

→ Solution

Challenge 2: An expect class

Write an expect class Logger with a single function fun log(message: String) in "commonMain" (as a comment-labeled section), then two "actual class Logger" implementations (labeled "androidMain" and "iosMain" in comments) — one prefixing messages with "[Android]", the other with "[iOS]". Write a shared function that takes a Logger and logs a test message.

Goal: Practice expect/actual on a class, not just a function.

→ Solution

Challenge 3: Designing a Shared/Platform Split

For a hypothetical note-taking app shared across Android and iOS via KMP, list (in comments, as a short design doc) which of the following belong in commonMain vs need an expect/actual split vs stay fully platform-specific: note data model, note validation logic, local file storage, the note list UI screen, network sync logic, push notification handling.

Goal: Practice reasoning about what's genuinely shareable in a KMP app, not just writing expect/actual syntax.

→ Solution

💡 Course 2 Complete — Straight Into Android Territory

Kotlin Intermediate closes here having covered coroutines, generics, delegation, DSLs, reflection, advanced patterns, and now multiplatform — genuinely everything needed to read and write real-world Kotlin, Android or otherwise. The planned Android Development courses build directly on this: Jetpack Compose leans on lambdas-with-receiver (DSL building) and sealed classes (Course 1) for UI state, ViewModels lean on coroutines and StateFlow, and KMP is directly relevant if a shared/cross-platform app is ever the goal.

🎯 What's Next

Kotlin Intermediate (Course 2) is complete. From here: Android Development Course 1 (Fundamentals) picks up directly where this leaves off, applying everything from both Kotlin courses to real Android Studio projects.