13. Move Android UI to KMP or migrate KMP to CMP

Daniyar Nurgaliyev
5 min readJun 23, 2024

--

In this guide, we’ll walk through the steps to migrate your Android UI to Compose Multiplatform (CMP) and make the necessary adjustments for dependencies and themes.

1. Adjust Plugins and Dependencies

First, ensure you have the correct version of the Gradle wrapper by updating the distribution URL in your gradle-wrapper.properties file:

...
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip

Note: Using Gradle 8.6-bin may cause errors, so it’s essential to use the specified version.

Update Dependencies

Modify the gradle/libs.versions.toml file to update and add necessary dependencies:

…/gradle/libs.versions.toml

[versions]
...
lifecycleVersion = "2.8.1" // rename only
coroutines = "1.8.1"
koinCompose = "1.1.5"

[libraries]
# rename only
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleVersion" }
# add new dependencies
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleVersion" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" }
# delete this dependency
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" }

[plugins]
...
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }

Update Android App Module

In the androidApp module's build.gradle.kts, remove the old dependency and add the new plugin:

…/androidApp/build.gradle.kts

plugins {
...
alias(libs.plugins.jetbrainsCompose)
}

dependencies {
// delete this
implementation(libs.koin.androidx.compose)
}

Root Build File Changes

Add the plugin in the root build.gradle.kts:

…/build.gradle.kts

plugins {
...
alias(libs.plugins.jetbrainsCompose) apply false
}

Maven Repository Addition

Include the Maven repository in settings.gradle.kts:

…/settings.gradle.kts

pluginManagement {
repositories {
...
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}
dependencyResolutionManagement {
...
repositories {
...
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

Shared Module Updates

Add the plugin and dependencies in the shared module’s build.gradle.kts:

…/shared/build.gradle.kts

plugins {
...
alias(libs.plugins.jetbrainsCompose)
}


kotlin {
...
sourceSets {
commonMain.dependencies {
// compose dependencies
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.components.resources)
implementation(compose.material3)
implementation(libs.compose.material)

// koin dependencies
...
implementation(libs.koin.compose)

// coroutine dependencies
implementation(libs.kotlinx.coroutines.core)
}
}
}

2. Modify Theme

Transfer theme files to the shared module to ensure consistency across platforms:

Create MyApplicationTheme.kt in the shared module:

…/shared/src/commonMain/kotlin/com/example/shared/ui/MyApplicationTheme.kt


@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
darkColorScheme(
primary = Color(0xFFBB86FC),
secondary = Color(0xFF03DAC5),
tertiary = Color(0xFF3700B3)
)
} else {
lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC5),
tertiary = Color(0xFF3700B3)
)
}
val typography = Typography(
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
)
val shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

MaterialTheme(
colorScheme = colors,
typography = typography,
shapes = shapes,
content = content
)
}

3. Migrate UI to CMP

Create ComposeMultiplatformApp.kt

Transfer the UI logic from MainActivity.kt to ComposeMultiplatformApp.kt in the shared module:

…/shared/src/commonMain/kotlin/com/example/shared/ui/ComposeMultiplatformApp.kt


@Composable
fun App() {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
UserScreen()
}
}
}


@Composable
fun UserScreen(
userViewModel: MainViewModel = koinInject(),
) {

val articlesState = userViewModel.allUsers.collectAsState(initial = emptyList())
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Button(onClick = {
val userName = (1..10).map { ('a'..'z').random() }.joinToString("")
userViewModel.insert(User(name = "$userName"))
}) {
Text(text = "Add User")
}

Button(onClick = {
if (articlesState.value.isNotEmpty()) {
userViewModel.update(
User(
id = articlesState.value[0].id,
name = "Updated ${articlesState.value[0].name}"
)
)
}
}) {
Text(text = "Update First User")
}

Button(onClick = {
if (articlesState.value.isNotEmpty()) {
userViewModel.delete(articlesState.value[0])
}
}) {
Text(text = "Delete First User")
}

Button(onClick = {
userViewModel.deleteAll()
}) {
Text(text = "Delete All Users")
}


LazyColumn(modifier = Modifier.weight(1f)) {
items(articlesState.value) { user ->
Text(text = user.name, style = MaterialTheme.typography.titleLarge)
}
}
}
}
}

It would still be 4 buttons and a list of items.

Update MainActivity.kt

Call the App() composable function from the shared module in MainActivity.kt:

…/androidApp/src/main/java/com/example/migrationtocmp/MainActivity.kt

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(modifier = Modifier.fillMaxSize()) {
App()
}
}
}
}
}

Update the iOS App

Create MainIOS.kt in the shared module for the iOS implementation:

…/shared/src/iosMain/kotlin/com/example/shared/MainIOS.kt


fun MainViewController() = ComposeUIViewController {
App()
}

Call the function from ContentView.swift in the iOS app project:

…/iosApp/iosApp/ContentView.swift

import SwiftUI
import shared

struct ContentView: View {
var body: some View {
ComposeView().ignoresSafeArea(.keyboard)
}
}

struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainIOSKt.MainViewController()
}

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

Delete Unused ViewModels

Remove injected ViewModels in KoinInitializer.kt if they were added earlier:

…/shared/src/iosMain/kotlin/com/example/shared/KoinInitializer.kt

fun initKoin() {
startKoin {
modules(platformModule() + sharedKoinModules)
}

// delete this class
class MainInjector: KoinComponent {
}
}

4. Running the Apps

Build and run the Android and iOS apps to see the updated UI and functionality.

Android app:

iOS app:

Enjoy 😉

Unlisted

--

--