Modern Crossplatform Desktop Applications with Kotlin Multiplatform and Compose UI
Kotlin is one of my favorite programming languages and with Kotlin Multiplatform (KMP) and Compose Multiplatform we can create beautiful modern desktop applications for Windows, MacOS and Linux from a single Kotlin codebase. In this post we will explore a template KMP project for desktop, customize the theme and export native binaries. The source code for this project is available on my GitHub.
Prerequisites
In the following parts I am assuming you have setup a working Kotlin Multiplatform development environment, for example with Fleet or Android Studio. If not, you can check the official guide to set it up for your system.
Setup
Start by creating your own repository from the project template in GitHub.
Clone the repository to your local workspace and open the project in your editor, I will be using Android Studio on Linux for this demo. Once the project is loaded, you should see a desktop run configuration if you are working with a JetBrains IDE.
Alternatively, you can run the Gradle command to start the app from the terminal in the project root directory.
./gradlew run
Now you should see the application window. If you click on the button, a greeting message will appear.
Exploring the Project
The Kotlin code is located under composeApp/src/desktopMain/kotlin. Here you will find the main entry point main.kt, the App.kt containing the Compose UI code and the Themes.kt and Type.kt with the app theme config. In the composeResources/drawable folder you will find the icon resource that is used for the window icon of the desktop application.
In the main.kt file the main application window is created. We need to specify that the application should close if the window is closed with the onCloseRequest parameter. We can customize the window with a title string and the icon, which we load from the drawable resources.
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "KMP Desktop App",
icon = painterResource(Res.drawable.icon)
) {
App()
}
}
Note: To access this resource class from the Kotlin code, we have to modify the configuration of the compose resources in the module build.gradle.kts file which is located at composeApp/build.gradle.kts. There we need to specify that the resource class must be public, otherwise it will be internal and we cannot access it from our own code. Also we need to make sure that the class is generated.
compose.resources {
publicResClass = true
generateResClass = always
}
In the App.kt we setup the Composables for the button with the “Click me!” text and the animated greeting that is toggled by the button click. We use the by syntax to directly extract the boolean value from the remembered mutable state, which is initially set to false.
@Composable
private fun AppContentGreeting() {
var showContent by remember { mutableStateOf(false) }
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
Column(
Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Hello Desktop!")
}
}
}
}
This is then wrapped in a Surface filling the full screen to make sure the background is colored according to the theme and this is again wrapped in the AppTheme. In the properties of the AppTheme, you can specify if a dark theme should be used.
Note: As of writing this post, on Linux the system dark theme mode does not work and is always unknown, therefore we have to manually specify the dark mode if we want it to appear.
@Composable
@Preview
fun App() {
AppTheme(
darkTheme = true,
) {
Surface(
Modifier.fillMaxSize()
) {
AppContentGreeting()
}
}
}
In the Theme.kt file the used AppTheme is defined with the two different color schemes, one for dark mode and one for light mode. If no arguments are passed to the constructors of the color scheme, the default theme colors will be used, as shown in the screenshots above.
Custom Theme
If you are creating a real project, you most likely don’t want to use the default color schemes, you want a color theme that represents your application. You could manually modify some of the colors in the theme, however this is both time consuming and hard to get right that it looks good or for example that there is enough contrast between the elements.
Instead a better approach is to use a theme palette generator. I’ve found this Material Theme Builder from the Material Foundation to be really practical and it even has en export option for compose.
On the left side you can upload an image to automatically generate a full color palette based on it or manually select colors below. On the right side there’s a live preview of full screens and if you scroll down, there are many different interactive components rendered to give you a feel for the colors.
Once you are happy with the color palette, you can click the icon at the right end of the toolbar to open the side bar and then in the bottom right corner click export and then select Jetpack Compose.
This will download a material-theme.zip file, which contains three Kotlin files under ui/theme. These files are not directly compatible with Compose Multiplatform, because they contain some android specific code. However, we can extract the relevant parts of the Color.kt file and from the Theme.kt file. From the colors file, we want all the colors for the normal contrast for light and dark mode. Copy these over to a new Colors.kt file in our project. We won’t use the other colors with the medium and high contrast.
val primaryLight = Color(0xFF6D5E0F)
val onPrimaryLight = Color(0xFFFFFFFF)
...
val surfaceContainerHighLight = Color(0xFFEEE8DA)
val surfaceContainerHighestLight = Color(0xFFE8E2D4)
val primaryDark = Color(0xFFDBC66E)
val onPrimaryDark = Color(0xFF3A3000)
...
val surfaceContainerHighDark = Color(0xFF2D2A21)
val surfaceContainerHighestDark = Color(0xFF38352B)
Now from the Theme file, copy the lightScheme and darkScheme variables.
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
...
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
...
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
Now you should get your new colors in your application.
Export Native Binaries
There are two ways to export the project if you want to distribute it as binaries. You can either package the project with an installer or distribute your application as a standalone executable that can be run without requiring an install.
Create Standalone Executable
To create a standalone executable, we need to run a Gradle task from the command line. Open a terminal at the root of the project and run the following command:
./gradlew createDistributable
This will create a desktop binary for your current operating system under /composeApp/build/compose/binaries/main/app/KMPDesktopApp/. Currently, cross-compiling is not supported so you have to create these distributables from the target platform. On Linux, the executable is in the bin sub folder of this output directory.
Create Installable Application
To create an installable application, you can use the package Gradle task:
./gradlew package
On my PopOS machine, this creates a .deb Debian package located at composeApp/build/compose/binaries/main/deb/. On Windows you can create MSI installer files and on MacOS you can generate DMG files, but you need to execute the command on the respective systems, as cross-compiling is not supported.
Conclusion
In this post we explored the power of Kotlin Multiplatform and Compose Multiplatform for creating modern desktop applications. By leveraging the capabilities and beauty of Kotlin and the intuitive UI framework of Compose, we can build visually appealing and responsive desktop applications for the Windows, Mac and Linux platform with a single Kotlin codebase.
Feel free to explore the project on my GitHub and use it as a starting point to create your own desktop application projects. Happy coding!