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
composeAppmodule. This contains both: the shared code and the android specific entrypoint (MainActivity).
Stricter approach would be to have a dedicatedsharedmodule for shared code. And anandroidAppmodule 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:
- has matching Objective-C header - so Swift would know where to invoke the functions.
- contains Kotlin/Native runtime Which will handle memory allocation, garbage collection, exceptions.
- contains Skia engine if CMP is used.
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
.swiftfile 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.