Compose Multiplatform in Action: Entry Points for Cross-Platform Android and iOS Code in CMP
Introduction
Compose Multiplatform (CMP)
Yesterday we gained a general understanding of the CMP project structure
From yesterday's Understanding CMP Project Structure and Build Configuration
we learned that in a CMP project we can write
Common logic in commonMain
Android platform logic in androidMain
iOS platform logic in iosMain
Desktop platform logic in desktopMain
YourProjectName
├── composeApp
│ ├── ...
│ ├── src
│ │ ├── commonMain
│ │ ├── commonTest
│ │ ├── iosMain
│ │ └── desktopMain
│ └── ...
├── ...
├── ...
└── ...
Now let's start understanding
the entry points of CMP
code
Since it involves cross-platform implementation
I feel it's important to understand
how the code works and how it enters each platform
so today I'll explain in detail the entry points for cross-platform code in CMP
Understanding CMP Code Entry Points
Let's first get a basic understanding of Compose Multiplatform and Kotlin Multiplatform
-
When CMP is created, these entry points are already established for you
You only need to
understand the concept
here -
The entry point for shared code in CMP's commonMain
is
expected
to be called by cross-platform codein androidMain, iOSMain, etc.
to achieve the goal of sharing code
-
Here we've created a shared App() function
which includes
-
A
custom common UI Theme
-
Using Koin for viewmodel injection
-
A custom Compose UI entry point
(In later chapters, we'll explain how to customize UI Theme, use Koin, customize Compose UI, and other topics)
-
// in ../commonMain/App.kt
@Composable
@Preview
fun App() {
ElegantAccessComposeTheme {
val viewModel = koinViewModel<MainViewModel>()
ElegantAccessApp(viewModel)
}
}
Android
actually calls the shared App() function from commonMain
// in ../androidMain/App.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val androidModule = module {
single<Activity> { this@MainActivity }
single<Context> { this@MainActivity.applicationContext }
}
startKoin {
modules(appModule() + androidModule)
}
setContent {
// Call the shared App() function we implemented
App()
}
}
}
-
In Android's
Android Manifest.xml
, thisMainActivity
is declared with an<activity>
tagalong with the initial page for launching the app via
<intent-filter>
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application ...> <activity ... android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
iosMain
actually calls the shared App() function from commonMain
// in ../commonIos/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
val uiViewController = LocalUIViewController.current
val iosModule = module {
single<UIViewControllerWrapper> { UIViewControllerWrapperImpl(uiViewController) }
}
KoinApplication(application = {
modules(appModule() + iosModule)
}) {
App()
}
}
- In
iOS
, the functionMainViewController()
from the aboveMainViewController.kt
is actually called
-
In
desktopMain
, the shared App() function from commonMain is actually calledusing the
application function
along withWindow
from compose to create a desktop application
// in ../desktopMain/main.kt
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "App",
) {
App()
}
}
-
The desktop component in CMP is also compiled through JVM
If you want to build it
use the following gradle command in your environment
./gradlew desktopRun -DmainClass=MainKt --quiet
- Alternatively, you can add this Gradle task directly to the Run Configuration in your IDE
<img src="/images/compose/046.png" alt="Cover" style="width: 90%"/ class="prose-img">
Developing Shared Logic
-
After understanding the entry points above
we can start developing shared logic
to achieve creating applications for multiple platforms with
just one set of code
-
As shown in the image below
we will spend most of our time in
./commonMain
most of the logic development happens here
except for things that depend on specific platforms, such as file systems, file pickers, etc.
which will be implemented through
expect
andactual
(we'll also cover how to use expect and actual in later chapters)
<img src="/images/compose/047.png" alt="Cover" style="width: 80%"/ class="prose-img">
-
However, up to this point
even though
desktop platforms
oriOS platforms
have their own file systemsrequiring custom implementations in the commonMain shared logic
in both
KMM
andCMP
there are already libraries that support writing these cross-platform components
using
kotlin
codeyou just need to
configure them in Gradle
For example: implementing file-related operations for desktop through Kotlin:
// ../desktop/PlatformFile.desktop.kt
actual class PlatformFile actual constructor(private val path: String) {
private val file = java.io.File(path)
actual fun exists() = file.exists()
actual fun createFile() = file.createNewFile()
actual fun writeBytes(bytes: ByteArray) = file.writeBytes(bytes)
actual fun delete() = file.delete()
actual fun isDirectory(): Boolean = file.isDirectory
actual fun copyTo(destination: PlatformFile, overwrite: Boolean) {
file.copyTo(java.io.File(destination.path), overwrite)
}
actual fun mkdirs() {
file.mkdirs()
}
}
actual class PlatformZip actual constructor() {
actual fun unzip(zipFilePath: String, destinationDir: String) {
java.util.zip.ZipFile(zipFilePath).use { zip ->
zip.entries().asSequence().forEach { entry ->
val file = java.io.File(destinationDir, entry.name)
if (entry.isDirectory) {
file.mkdirs()
} else {
file.parentFile.mkdirs()
zip.getInputStream(entry).use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
}
}
}