MediumMy First Deep Dive into Kotlin Multiplatform & Compose Multiplatform: Understanding the Architecture Instead of Just Running the App | by Neeraja Gandla | Dev Genius
Sitemap
Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

My First Deep Dive into Kotlin Multiplatform & Compose Multiplatform: Understanding the Architecture Instead of Just Running the App

6 min readJun 25, 2026

--

Press enter or click to view image in full size

Over the last few weeks, I’ve been thinking about what technology I should invest my time in next.

As a Senior Android Developer, I evaluated several cross-platform options including Flutter, React Native, Ionic and Kotlin Multiplatform.

One thing became clear quickly: learning Kotlin Multiplatform (KMP) and Compose Multiplatform (CMP) gives me the highest return on my existing Android expertise.

Unlike Flutter, I don’t have to learn a new language.

Unlike React Native, I don’t have to switch to the JavaScript ecosystem.

Instead, I can continue leveraging:

  • Kotlin
  • Compose
  • Coroutines
  • Flow
  • Android Studio
  • Gradle

while gradually expanding into cross-platform development.

Today was my first real deep dive into understanding how a Compose Multiplatform project is actually structured.

Instead of immediately building features, I decided to understand the architecture first.

And that turned out to be one of the best decisions.

Step 1: Running the Same App on Android and Desktop

The first thing I did was run the sample Compose Multiplatform application.

I had already managed to run it as a Desktop application earlier.

Today I successfully ran the same application on my Android device.

At this point I had not changed a single line of code.

The same UI was running on:

  • Android
  • Desktop

without any platform-specific UI implementation from me.

That immediately raised a question:

How is this actually working?

Press enter or click to view image in full size

Step 2: Looking at the Project Structure

The project structure looked something like this:

Press enter or click to view image in full size

The first thing that caught my attention was:

shared
├── commonMain
├── androidMain
├── iosMain
└── jvmMain

androidApp
iosApp
desktopApp

Initially this confused me.

If there is already:

  • androidApp
  • iosApp

then why do we need:

  • androidMain
  • iosMain

inside the shared module?

Wouldn’t the platform-specific code belong inside the platform-specific app modules?

That question eventually led me to one of the biggest KMP concepts.

Step 3: Understanding Source Sets

The key realization was that:

androidApp
iosApp
desktopApp

are mostly launcher modules.

Their primary job is to start the application.

The actual application code mostly lives inside:

shared

Inside shared, Kotlin organizes code using source sets:

commonMain

Contains code shared by all platforms.

Examples:

  • Compose UI
  • ViewModels
  • Business Logic
  • Repositories
  • Networking
  • State Management

androidMain

Contains Android-specific implementations.

Examples:

  • SharedPreferences
  • DataStore
  • Android Notifications
  • CameraX
  • Android Permissions

iosMain

Contains iOS-specific implementations.

Examples:

  • NSUserDefaults
  • Keychain
  • CoreLocation
  • AVFoundation

jvmMain

Contains Desktop-specific implementations.

This was my first major “aha!” moment.

The platform-specific logic still belongs inside the shared module because it is part of the application’s behavior.

The launcher modules should ideally remain thin.

Step 4: Why Can’t We Put Everything in commonMain?

My next question was:

Why do androidMain and iosMain even exist?

Why not put everything inside commonMain?

The answer is that commonMain can only use APIs that exist on every target.

For example:

Text("Hello")

works everywhere.

But:

SharedPreferences

exists only on Android.

And:

NSUserDefaults

exists only on iOS.

Therefore platform-specific implementations must live in their respective source sets.

This is where KMP’s expect/actual mechanism becomes important.

A shared API can be declared in commonMain and implemented differently in androidMain and iosMain.

Step 5: How Does Android Studio Suddenly Understand iOS APIs?

This was another interesting question I had.

Normally Android Studio would complain if I typed:

NSUserDefaults

inside a regular Android project.

So what changed?

The answer lies in Kotlin Multiplatform targets.

Inside the Gradle configuration I found:

iosArm64()
iosSimulatorArm64()

These targets tell Kotlin:

Get Neeraja Gandla’s stories in your inbox

Join Medium for free to get updates from this writer.

“Generate iOS builds too.”

Kotlin/Native then exposes Apple frameworks to Kotlin.

As a result:

import platform.Foundation.NSUserDefaults

becomes available inside iosMain.

Another “aha!” moment:

Android Studio itself hasn’t magically learned iOS.

Kotlin Multiplatform has taught the compiler about multiple target platforms.

Step 6: Reading the Gradle Configuration

I spent significant time understanding the shared module’s build.gradle.kts.

One interesting section was:

listOf(
iosArm64(),
iosSimulatorArm64()
)

This creates:

  • Real iPhone builds
  • iOS Simulator builds

I also noticed:

baseName = "Shared"

Initially I thought this was related to the shared module name.

The actual purpose is different.

Kotlin generates:

Shared.framework

for iOS.

The generated framework can later be imported from Swift as:

import Shared

That was another useful realization.

The framework name is configurable and doesn’t necessarily have to match the module name.

Step 7: Understanding How Compose UI Reaches Each Platform

I then traced the flow of a composable such as:

App()

Android

MainActivity
↓
setContent
↓
App()

Desktop

main()
↓
Window
↓
App()

iOS

This path was more interesting.

iOSApp.swift
↓
ContentView.swift
↓
ComposeView
↓
MainViewControllerKt.MainViewController()
↓
ComposeUIViewController
↓
App()

This was one of the biggest architecture discoveries of the day.

Step 8: Why Is App() Called from iosMain Instead of iosApp?

At first I thought this was inconsistent.

Android calls App() from androidApp.

Desktop calls App() from desktopApp.

But iOS seemed to call App() from iosMain.

After tracing the flow, I realized what’s happening.

Swift cannot directly render a Compose Composable.

A bridge is required:

ComposeUIViewController {
App()
}

This bridge is iOS-specific.

Therefore it lives inside:

iosMain

Swift simply hands control over to Kotlin.

That design keeps most of the implementation in Kotlin instead of Swift.

Another major “aha!” moment.

Press enter or click to view image in full size

Step 9: The Mystery of MainViewControllerKt

One detail caught my attention.

Swift was calling:

MainViewControllerKt.MainViewController()

instead of:

MainViewController()

Why the extra “Kt”?

The answer is surprisingly simple.

Top-level Kotlin functions are compiled into generated classes.

For a file named:

MainViewController.kt

Kotlin generates something conceptually similar to:

MainViewControllerKt

which contains the function as a static method.

This is why Swift sees:

MainViewControllerKt.MainViewController()

instead of directly calling the function.

Step 10: Why Android Studio Shows “No Usages”

Another interesting observation:

Android Studio showed no usages for:

fun MainViewController()

even though Swift was clearly calling it.

The reason is that IDE usage analysis works best for:

Kotlin → Kotlin
Java → Kotlin

but this call path is:

Swift
↓
Generated Framework
↓
Kotlin

The function is definitely used at runtime, but the IDE cannot always trace that usage across language boundaries.

Step 11: Comparing KMP with Other Cross-Platform Frameworks

While learning KMP, I also revisited the broader cross-platform landscape.

Cordova / PhoneGap

Web app inside a WebView.

Ionic

Modern web framework inside a WebView.

React Native

JavaScript bridge talking to native UI.

Flutter

Single Dart codebase rendered through Flutter’s own engine.

Kotlin Multiplatform

Originally focused on:

Shared Logic
Native UI

Compose Multiplatform

Extends that vision to:

Shared Logic
+
Shared UI

For Android developers, KMP has a unique advantage:

Every hour invested in KMP also strengthens Kotlin and Android expertise.

Final Thoughts

The biggest lesson from today was that Kotlin Multiplatform is not just about sharing code.

It’s about carefully deciding:

  • What should be shared
  • What should remain platform-specific
  • How platform boundaries are managed
  • How Kotlin integrates with native ecosystems

The project structure that initially looked confusing now makes much more sense.

Today’s goal wasn’t to build features.

It was to understand the architecture.

And that foundation will make future KMP learning significantly easier.

--

--