Techbase Academy
Android · Lesson 66

Why Kotlin Multiplatform

13 phasesCandidate Analysis: What Would You Share?Android Studio
Phase 1 of 13
Lesson Introduction

Suppose your school-fees app is a success on Android, and the bursar's office asks for an iPhone version, and the head teacher wants a desktop version for the school computer. The traditional answer is to write the app again in Swift for iOS, with its own bugs, its own business rules and its own team. When the fee rule changes, you change it twice, and the two apps slowly drift apart.

Kotlin Multiplatform (KMP) offers another answer: write the parts that are the same once, in Kotlin, and compile them for every platform, while keeping each platform's own UI and system APIs where that matters. This lesson opens the Kotlin Multiplatform programme at https://developer.android.com/courses/pathways/kotlin-multiplatform (Why you should adopt Kotlin Multiplatform and Get started with Kotlin Multiplatform). The programme links to the JetBrains documentation at https://www.jetbrains.com/help/kotlin-multiplatform-dev/ and to Google's guidance at https://developer.android.com/kotlin/multiplatform.

By the end you will be able to:

  • Explain what KMP is and how it differs from "write once, run anywhere" frameworks.
  • Describe how Kotlin compiles to each target.
  • Decide what to share and what to keep native.
  • Use expect/actual and interfaces to reach platform APIs.
  • Compare KMP, Compose Multiplatform, Flutter and React Native fairly.
  • Make an honest adopt / do not adopt decision for a project.

Big idea: KMP is about sharing the right code, not all the code. Share the logic that is identical, and let each platform keep what is special.

Honesty note: the KMP tooling changes quickly (Gradle plugins, the Android target, which libraries support which targets). This lesson explains stable ideas and shows code in the style of current documentation, but check the official pages for the current setup before starting a real project. The ten-style list of reasons below is my own summary of the common arguments; JetBrains publishes its own list at https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-reasons-to-try.html, and you should read it too.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 13
Prerequisite Concepts
  • Kotlin classes, interfaces and functions (lessons 6 to 14).
  • Coroutines and Flow (lesson 33).
  • App architecture and layers (lessons 60 to 65), because the layered design decides what is easy to share.
  • Gradle Kotlin DSL and version catalogs (lessons 3 and 34).
  • Room and Retrofit (lessons 29 and 34), which you will meet again in KMP form.
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 13
Conceptual Understanding: What Is Kotlin Multiplatform?

One language, many targets

A target is a platform your code is compiled for. Kotlin can compile to several:

Target Kotlin compiles to Typical use
Android JVM bytecode (then Android's runtime) Your Android app
JVM / Desktop JVM bytecode Desktop apps, servers
iOS (and macOS, watchOS, tvOS) Native machine code via Kotlin/Native (LLVM) iPhone and iPad apps
Web JavaScript or WebAssembly Browser apps
Linux, Windows Native code Command-line and desktop tools

You write common Kotlin that uses only the Kotlin standard library and multiplatform libraries. The Kotlin compiler produces the correct output for each target. On iOS your shared code becomes a framework that Swift code imports, like any other iOS library.

Where the code lives

A KMP module is split into source sets:

text
shared/
  src/
    commonMain/kotlin/       code for ALL targets
    commonTest/kotlin/       tests for common code
    androidMain/kotlin/      Android-only code
    iosMain/kotlin/          iOS-only code

Common code cannot call Android or iOS APIs directly, because it must compile everywhere. When it needs a platform feature, you use expect/actual or an interface (below).

What KMP is not

  • It is not a UI toolkit. By default, your Android app still uses Compose and your iOS app still uses SwiftUI. (Compose Multiplatform is a separate JetBrains framework that can share UI as well; see the comparison below.)
  • It is not "all or nothing". You can share a single module, a single class, or the whole data layer, in an existing app.
  • It does not hide the platform. You still build and ship an Android app and an iOS app, each with its own entry point.
✏️ Your Task
Practise what you just learned about Conceptual Understanding: What Is Kotlin Multiplatform?. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 13
Reasons to Adopt KMP (and Honest Limits)

The usual arguments, in my own words:

Reason What it means
Share business logic Fee rules, validation and sync run on both platforms and cannot drift apart
Native UI stays native Each platform keeps its own look, accessibility and performance
Incremental adoption Start with one module in an app you already have
One language and one set of skills Android developers read and review the shared code
Fewer duplicate bugs Fix once; both apps benefit
Shared tests Test business rules once in commonTest
Real libraries exist Coroutines, serialization, Ktor, Room, DataStore and more support multiple targets
Strong backing JetBrains develops it; Google supports KMP for sharing business logic on Android and iOS
Interop, not a sandbox You can call platform APIs and use platform libraries when needed
Freedom to share more later Move from logic-only to shared UI with Compose Multiplatform if it suits you

Limits to be honest about:

  • iOS builds need a Mac with Xcode, always. You can learn and ship the Android half on any computer, but running the iOS app needs Apple's tools.
  • Not every library is multiplatform. Some Android libraries have no iOS version, so you find alternatives or use expect/actual.
  • Build setup is more complex than a single-platform app, and tooling changes between releases.
  • The team needs to cooperate. iOS developers must be willing to use a Kotlin-built framework, and Kotlin/Swift interop has some rough edges (for example with generics and sealed classes).
  • It is not always worth it. An app with little shared logic, or a team already happy with two native codebases, may gain little.
✏️ Your Task
Practise what you just learned about Reasons to Adopt KMP (and Honest Limits). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 13
Step-by-Step Build 1: Common Code and Its Platforms

Business logic that needs nothing special

Our FeeTracker rule from lesson 63 uses only Kotlin, so it can move to commonMain unchanged:

kotlin
// shared/src/commonMain/kotlin/com/example/fees/FeeSummary.kt
package com.example.fees
data class FeeSummary(val paid: Long, val balance: Long) {
 val isOwing: Boolean get() = balance > 0
}
class CalculateFeeSummary(private val termFee: Long = 450_000) {
 operator fun invoke(payments: List<Long>): FeeSummary {
 val paid = payments.sum()
 return FeeSummary(paid = paid, balance = termFee - paid)
 }
}

Why this works: no Android imports, no java.* classes (those do not exist on iOS), just Kotlin's standard library. This is the strongest argument for the plain Kotlin domain layer in lesson 63: it is already shareable.

A test in commonTest runs on every target:

kotlin
// shared/src/commonTest/kotlin/com/example/fees/FeeSummaryTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FeeSummaryTest {
 @Test
 fun balance_is_term_fee_minus_payments() {
 val summary = CalculateFeeSummary(termFee = 100)(listOf(30, 20))
 assertEquals(50, summary.balance)
 assertTrue(summary.isOwing)
 }
}

Notice kotlin.test, not JUnit: JUnit is a JVM library, so common tests use the multiplatform kotlin.test API. The same test runs on the JVM (Android host tests) and, on a Mac, on the iOS simulator.

What you cannot use in common code

Do not use in commonMain Use instead
java.util.*, java.time.*, java.io.* kotlin.collections, kotlinx-datetime, kotlinx-io or Okio
String.format(...) String templates, or a multiplatform formatting library
android.* classes expect/actual or an interface
Retrofit, OkHttp Ktor client
Gson or Moshi kotlinx.serialization
SharedPreferences DataStore (multiplatform) or an interface
LiveData Flow / StateFlow

Most of your lessons already used the multiplatform-friendly choices (coroutines, Flow, kotlinx.serialization), so your code is closer to KMP than you might think.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Common Code and Its Platforms. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 13
Step-by-Step Build 2: Reaching the Platform with `expect`/`actual`

Sometimes common code needs something only the platform knows, such as the device name or the current time zone. Declare what you need in common code, and supply how on each platform.

kotlin
// commonMain/Platform.kt
expect fun platformName(): String
fun greeting(): String = "Hello from ${platformName()}!"
kotlin
// androidMain/Platform.android.kt
import android.os.Build
actual fun platformName(): String = "Android ${Build.VERSION.SDK_INT}"
kotlin
// iosMain/Platform.ios.kt
import platform.UIKit.UIDevice
actual fun platformName(): String =
 UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
  • expect is a promise: "every target will provide this".
  • actual is the fulfilment in a platform source set. If a target lacks an actual, the build fails, so you cannot forget one.
  • greeting() lives in common code and works everywhere, calling the right actual at run time.
  • On iOS, Kotlin has access to Apple's frameworks through the platform.* packages (here UIKit).

expect/actual also works for classes:

kotlin
// commonMain
expect class Clock() {
 fun nowMillis(): Long
}
kotlin
// androidMain
actual class Clock actual constructor() {
 actual fun nowMillis(): Long = System.currentTimeMillis()
}

Often better: an interface

expect/actual is compile-time and rigid. For most application code, a plain interface in common code, with implementations supplied by each platform app, is more flexible and easier to test:

kotlin
// commonMain
interface TokenStorage {
 suspend fun read(): String?
 suspend fun write(token: String)
}
class LoginUseCase(private val storage: TokenStorage, private val api: AuthApi) {
 suspend operator fun invoke(user: String, password: String): Result<Unit> = runCatching {
 val token = api.login(user, password)
 storage.write(token)
 }
}
kotlin
// androidMain
class AndroidTokenStorage(private val context: Context) : TokenStorage {
 override suspend fun read(): String? = /* EncryptedSharedPreferences or DataStore */ null
 override suspend fun write(token: String) { /* ... */ }
}
swift
// iosApp (Swift): Keychain implementation of the same Kotlin interface
class KeychainTokenStorage: TokenStorage {
 func read() async throws -> String? { /* Keychain lookup */ return nil }
 func write(token: String) async throws { /* Keychain save */ }
}

The shared LoginUseCase depends on the interface, not the platform. This is the dependency inversion idea from lessons 60 and 65 doing real work. A rule of thumb from experience: use expect/actual for tiny, stateless platform facts (a platform name, a UUID generator), and interfaces plus injection for services (storage, notifications, biometrics).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Reaching the Platform with `expect`/`actual`. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 13
Step-by-Step Build 3: How Compilation Works

Understanding the pipeline removes much of the mystery.

text
commonMain  ─┐
             ├─► compiled for Android ──► .class bytecode ─► your Android app
androidMain ─┘

commonMain  ─┐
             ├─► compiled for iOS ─────► native code ───────► shared.framework ─► Xcode app
iosMain     ─┘
  1. For each target, the Kotlin compiler combines commonMain with that target's own source set.
  2. For Android and JVM, the output is bytecode, exactly as in any Android module.
  3. For iOS, Kotlin/Native compiles to machine code and packages it as a framework (and, with Swift Package Manager export, an XCFramework: lesson 69). Swift sees Kotlin classes as Objective-C-style classes with a generated header.
  4. Dependencies must also have a version for each target. A multiplatform library publishes a common variant plus per-target artifacts, and Gradle picks the right ones automatically.

The Gradle setup is a short kotlin { } block. You will study it line by line in lesson 67; here is a preview so that it does not look strange later:

kotlin
// shared/build.gradle.kts (preview; see lesson 67 for the current Android target setup)
plugins {
 alias(libs.plugins.kotlin.multiplatform)
 alias(libs.plugins.android.kotlin.multiplatform.library)
}
kotlin {
 android {
 namespace = "com.example.fees.shared"
 compileSdk = 36
 minSdk = 24
 }
 iosArm64()
 iosSimulatorArm64()
 sourceSets {
 commonMain.dependencies {
 implementation(libs.kotlinx.coroutines.core)
 }
 commonTest.dependencies {
 implementation(kotlin("test"))
 }
 }
}
✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: How Compilation Works. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 13
Step-by-Step Build 4: What to Share

A practical way to choose, layer by layer, using the architecture from lessons 60 to 65:

Layer Share? Notes
Domain (use cases, rules, models) Yes, first Plain Kotlin; highest value, lowest risk
Data: networking Yes Ktor client and kotlinx.serialization
Data: database Often Room supports KMP (lesson 70); SQLDelight is another popular option
Data: repositories Yes Depend on multiplatform sources
Preferences/storage Yes with care DataStore is multiplatform
UI-layer state holders (ViewModels) Possible Lifecycle ViewModel has multiplatform support; check the docs
UI Optional Native UI per platform, or Compose Multiplatform
Platform integrations (push, camera, biometrics, widgets) No Keep native behind interfaces

Start small. The recommended adoption path is: (1) share a pure-Kotlin domain module, (2) share networking and models, (3) share the database, (4) consider shared state holders and, only if you want to, shared UI.

What a good first candidate looks like

  • It has clear inputs and outputs (a function from payments to a summary).
  • It has no UI and no platform calls.
  • It is already tested and stable.
  • Both teams care about consistency (money calculations, validation, date rules).
✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: What to Share. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 13
Step-by-Step Build 5: Fair Comparisons
Kotlin Multiplatform Compose Multiplatform Flutter React Native
Language Kotlin Kotlin Dart JavaScript/TypeScript
Shares Logic (UI optional) Logic and UI Logic and UI Logic and UI
UI approach Native UI per platform Compose-style UI drawn by the framework on each platform Own rendering engine Native views driven by JS
Native look and feel Fully native Compose look on every platform (customisable) Own widgets, can imitate platforms Native components
Adopt in an existing app Very easy, module by module Possible Harder (embedding "add-to-app") Possible
Best when You have native apps and want to share logic You want one UI codebase with Kotlin Greenfield app, one team, custom UI Web-skilled team, shared JS code

Points of fairness:

  • Compose Multiplatform is built on Kotlin Multiplatform and is developed by JetBrains. Its iOS support has been reaching stable status in recent releases; check the current status page before relying on any particular platform.
  • None of these is "the best". A team of Android developers with an existing native app usually finds KMP the smoothest path; a startup with no native code and a small team may prefer a single-UI framework.
  • Whatever you choose, measure with a small prototype before committing, as lesson 57 taught for performance.
✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Fair Comparisons. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 13
Step-by-Step Build 6: Your Own Adoption Decision

Create a short decision record for a project. A good one answers:

text
Project:        FeeTracker (Android today; iOS and desktop requested)
Shared code:    domain rules, networking models, repositories, database
Native code:    UI, notifications, camera for receipts
Team:           3 Android developers, 1 iOS developer
Risks:          iOS developer unfamiliar with Kotlin framework; one Android library has no iOS version
Mitigation:     pair on the first shared module; replace the library with a multiplatform one
First step:     move CalculateFeeSummary and models to :shared, tests green on both platforms
Decision:       Adopt incrementally. Review after the first release.

Writing it down forces honest answers and makes the review easy later.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 6: Your Own Adoption Decision. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 13
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Shareable or not?

For each, decide whether it can live in commonMain as written, and what to change if not: (a) fun today() = java.time.LocalDate.now().toString(); (b) class Student(val id: Int, val name: String); (c) fun toNaira(kobo: Long) = kobo / 100; (d) val prefs = context.getSharedPreferences("x", 0).

(a) No: java.time is JVM-only. Use kotlinx-datetime's Clock.System.todayIn(TimeZone.currentSystemDefault()), or an interface. (b) Yes. (c) Yes. (d) No: Context and SharedPreferences are Android. Hide storage behind an interface (such as TokenStorage) or use multiplatform DataStore.

🎯 Your Challenge

Exercise 2: expect/actual or interface?

Choose a mechanism for each: (a) returning the platform name; (b) a biometric prompt with a callback result; (c) generating a random UUID string.

(a) expect/actual function: tiny, stateless. (b) An interface (BiometricAuthenticator) implemented natively and injected, because it involves UI, lifecycle and callbacks. (c) expect/actual (or Kotlin's own kotlin.uuid.Uuid where available in your Kotlin version), because it is a small stateless utility.

🎯 Your Challenge

Exercise 3: Honest advice

A friend wants to rewrite a working Android-only app in KMP "because everyone is doing it". The app has no iOS version planned. What do you advise?

Do not rewrite for fashion. Without a second platform, the main benefit (sharing) is absent, and the migration costs time and adds build complexity. Keep a clean layered design (lessons 60 to 65), so that moving the domain layer into a shared module later is cheap, and revisit KMP when a second platform becomes a real requirement.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 13
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Importing java.* or android.* in common code. Fix: use multiplatform libraries or interfaces.

Mistake #2: Trying to share everything on day one. Fix: start with the domain layer.

Mistake #3: Using JUnit in commonTest. Fix: use kotlin.test.

Mistake #4: Overusing expect/actual. Fix: prefer interfaces for services.

Mistake #5: Forgetting the iOS side needs a Mac. Fix: plan hardware and CI early.

Mistake #6: Choosing libraries without checking target support. Fix: check klibs.io or the library's docs (lesson 69).

Mistake #7: Treating KMP as a UI framework. Fix: remember it shares logic; Compose Multiplatform shares UI.

Mistake #8: Copying outdated Gradle setups from old blog posts. Fix: follow the current official pages.

Mistake #9: Skipping tests in shared code. Fix: test in commonTest; one test runs on every target.

Mistake #10: Adopting without a decision record. Fix: write down scope, risks and a first step.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 13 of 13
Knowledge Check

Question 1. What does KMP compile common Kotlin into for Android and for iOS?

JVM bytecode for Android, and native machine code packaged as a framework (via Kotlin/Native) for iOS.

Question 2. What is the difference between expect and actual?

expect declares in common code what is needed; actual provides the implementation in each platform source set.

Question 3. Why is the domain layer the best first candidate to share?

It is plain Kotlin with no platform dependencies, has high value (rules stay consistent) and is easy to test.

Question 4. Name two things KMP does not do.

It does not provide a UI toolkit by itself (that is Compose Multiplatform), and it does not remove the need for a Mac and Xcode to build iOS apps.

Question 5. Which test library works in commonTest?

kotlin.test.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Candidate Analysis: What Would You Share?

Project goal: Analyse one of your own apps and produce a KMP adoption plan, plus a small proof-of-concept of shareable code.

Requirements:

  1. Pick an app you have built in this course (FeeTracker, ClassRegister, or another).
  2. List every class and mark it Share, Share with changes or Keep native, with one line of reasoning.
  3. List every library you use and mark whether a multiplatform alternative exists (check klibs.io and each library's documentation).
  4. Write the decision record in the format of Build 6.
  5. Take one pure-Kotlin class and rewrite it using only the Kotlin standard library, so it could live in commonMain. Write its test using kotlin.test.
  6. Write one expect/actual pair and one interface-based design for two different platform needs, and justify each choice.

Stretch goal: estimate what percentage of your code is shareable by lines of code per layer, and explain which layer would give the best return.

A worked analysis for FeeTracker (example):

text
Class                        Verdict               Reason
Payment, Student             Share                 plain data classes
CalculateFeeSummary          Share                 pure rule, tested
GetStudentBalancesUseCase    Share                 depends only on repository interfaces
PaymentsRepository (iface)   Share                 interface only
DefaultPaymentsRepository    Share with changes    swap Retrofit for Ktor, Room for Room KMP
PaymentApi (Retrofit)        Share with changes    rewrite as a Ktor client class
Room entities and DAOs       Share with changes    Room KMP (lesson 70)
FeeViewModel                 Share with changes    use multiplatform lifecycle ViewModel, or keep native
Composables                  Keep native           (or Compose Multiplatform later)
Notification code            Keep native           interface + Android/iOS implementations

A proof-of-concept class and test using only the standard library:

kotlin
// commonMain
class Percentage(private val part: Long, private val whole: Long) {
 init { require(whole > 0) { "whole must be positive" } }
 fun asWholeNumber(): Int = ((part * 100) / whole).toInt()
}
// commonTest
class PercentageTest {
 @Test fun half_is_fifty() = assertEquals(50, Percentage(50, 100).asWholeNumber())
 @Test fun zero_whole_is_rejected() {
 assertFailsWith<IllegalArgumentException> { Percentage(1, 0) }
 }
}

Library check (example):

text
Retrofit/OkHttp   -> Ktor client (multiplatform)
Gson/Moshi        -> kotlinx.serialization
Room 2.x          -> Room KMP (check setup page; lesson 70)
Hilt              -> Android-only: keep DI per platform, or use Koin/kotlin-inject in shared code
Coil              -> Coil 3 supports multiplatform (verify targets before relying on it)
1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 66 project”.

3

On github.com click New repository, name it android-lesson-66-why-kotlin-multiplatform, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 66 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: What Is Kotlin Multiplatform?
  • Reasons to Adopt KMP (and Honest Limits)
  • Step-by-Step Build 1: Common Code and Its Platforms
  • Step-by-Step Build 2: Reaching the Platform with `expect`/`actual`
  • Step-by-Step Build 3: How Compilation Works
  • Step-by-Step Build 4: What to Share
  • Step-by-Step Build 5: Fair Comparisons
  • Step-by-Step Build 6: Your Own Adoption Decision
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close