Skip to content
Go back

KMP Primer

Published:

Table of Contents

Open Table of Contents

What is it?

A way to write code that runs on multiple platforms, like Android and iOS.

How does that work?

You set up KMP plugin on your kotlin/android project. This allows for generating .XCFramework artifact which the iOS app consumes.

iOS KMP case

Now when developing iOS app, you’ll be writing native SwiftUI screens. But the viewmodels can be invoked from .XCFramework artifact produced by KMP. Meaning the viewmodels, with rest of the business logic, can come from the shared KMP project.

// Example of how SwiftUI can interact with shared ViewModel
import ComposeApp

@MainActor
class iOSViewModel: ObservableObject {

    // Init the KMP ViewModel
    private let sharedVM = SharedViewModel()
    
    @Published var uiMessage: String = ""
    
    func fetchGreeting() {
        uiMessage = sharedVM.getGreeting() // <- Interact with VM
    }
}

struct ContentView: View {
    @StateObject private var viewModel = iOSViewModel()

    var body: some View {
        VStack {
            Text(viewModel.uiMessage)
            Button("Invoke KMP") {
                viewModel.fetchGreeting()
            }
        }
    }
}

iOS CMP case

When using Compose Multiplatform you are also sharing the UI and navigation logic. Now the .XCFramework artifact will contain an entrypoint that renders the whole screen in Skia engine. Thus the iOS app will become a relatively thin host. The main UiViewController will just invoke the CMP ComposeView(). That’s it.

// The main App class of iOS
@main
struct iOSApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView() // <- The only view
        }
    }
}
// The definition of main ContentView which calls CMP
import ComposeApp // <- Import the KMP/CMP library

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainViewControllerKt.MainViewController() // <- Hands control to CMP
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea()
    }
}
// The Kotlin definition of the MainViewController
fun MainViewController() = ComposeUIViewController {
    App() // <- ViewController that calls our Compose impl.
}

Note: in both cases you may still need to write some native logic if its platform specific. Also in both cases you are in control if and when you want to invoke KMP code. Even in CMP you can jump to native SwiftUI with a little setup.

Android KMP/CMP case

Your MainActivity will directly invoke the shared code. There is no artifact being passed around, as the KMP modules are Kotlin modules, which can be compiled directly to the android app.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)

        setContent {
            App() // <- Calls Compose impl directly.
        }
    }
}

Note: in the latest Android Studio template: you will get composeApp module. This contains both: the shared code and the android specific entrypoint (MainActivity).
Stricter approach would be to have a dedicated shared module for shared code. And an androidApp module purely for android entrypoint.

Shared Code

Even though shared code is written in Kotlin, you can’t write it like an Android app. On iOS it doesn’t run on JVM/ART, so avoid Android APIs (e.g., Context, android.util.Base64, android.net.Uri) and JVM-only APIs (many java.*). Prefer the Kotlin standard library and multiplatform libraries instead.

Platform specific code

So if you need to have some platform specific implementation (like access Context in android). KMP has expect - actual constructs. Very roughly its like declaring an interface in the common sourceset, and implementing it in the platform specific sourceset.

// commonMain sourceset
expect fun platform(): String // <- Expect such function named "platform"
// iosMain sourceset
actual fun platform() = "iOS"
// androidMain sourceset
actual fun platform() = "Android"

XCFramework

It’s a package that contains binaries for different platforms (such as iOS and iOS simulator). Allows the build system to grab the binary with correct architecture.

Note “framework” is a bit loaded name, here it’s not an architectural framework that controls your code, it’s just a compiled package/library.

KMP XCFramework artifact

Contains your compiled KMP binaries for each build target. Each binary also:

Meaning iOS apps get 2 garbage collectors, one by Swift (ref counting), other for Kotlin (tracing).

Note since Kotlin 2.2.20 Swift Export is available. Instead of Objective-C header, KMP improved to generate a .swift file as the bridge instead.

KMP libraries

Instead of building whole apps its also possible to publish just a library thats written in KMP. Big caveats apply.

Android Library

In this case its packaged as .aar. Using it is like any other library dependency, for android project it looks just like another library.

iOS library

It’s still packaged as .XCFramework. Using it is like any other library. But, since it brings its own runtime, you should not include multiple separate KMP libraries. KMP produces a helpful assert if you manage to run app in that state: runtime assert: runtime injected twice;.

Libraries solution

If you want to include multiple libraries from KMP, you have to bundle them together as a single KMP library (a single XCFramework artifact). You can include external KMP libraries in there also. This is called an umbrella module.



Next Post
MVVM damage just by bad naming?