Build Desktop Apps With Kotlin And Jetpack Compose For Desktop

Introduction
Compose for Desktop is a UI framework made by Jetbrains to simplify and accelerate desktop application development using Kotlin with a reactive and functional API. It targets the JVM and thus can be used to build cross platform GUI apps. It's currently in alpha.
It's based on the Jetpack Compose declarative UI Toolkit made by Google for Android. Using Kotlin to design user interfaces promotes less bugs, better tooling support and more concise and robust code. This is achieved using a declarative UI model based on composable functions, which take in parameters to describe the UI logic without returning anything and must be free of side-effects. More details about composables are presented in the Compose mental model documentation.
Although this tutorial's main subject is Compose for Desktop, but the concepts and most code snippets are also applicable to Android's Jetpack Compose.
Getting started
JDK 11 or later is required. IntelliJ IDEA Community Edition or Ultimate Edition 20.2 or later are the preferred IDEs. Follow this tutorial if you need to install JDK 11 and IntelliJ IDEA.
Download the Compose for Desktop project starter from the following link
Extract and move the folder to your workspace
Open IntelliJ IDEA and create a new project from existing sources
Choose Gradle, and click Finish
Open
src/main/kotlin/main.kt
, override the content with the following:import androidx.compose.desktop.Window import androidx.compose.material.Text fun main() = Window { Text("Hello, World!") }
Open the terminal from the project's root, and launch:
gradlew run
. You can also launch therun
gradle task directly from IntelliJ IDEA.




Learning by practice: implementing a simple calculator
To dig deeper into Compose for Desktop concepts and learn it by practice, we will implement in this section a calculator UI. The resulting UI will look like this:

Entry point
const val DEFAULT_WIDTH = 500
const val DEFAULT_HEIGHT = 500
fun main() = Window(
title = "Compose Calculator - simply-how.com",
size = IntSize(DEFAULT_WIDTH, DEFAULT_HEIGHT),
icon = Assets.WindowIcon
) {
MaterialTheme(colors = lightThemeColors) {
val mainOutput = remember { mutableStateOf(TextFieldValue("0")) }
Column(Modifier.fillMaxHeight()) {
DisplayPanel(
Modifier.weight(1f),
mainOutput
)
Keyboard(
Modifier.weight(4f),
mainOutput
)
}
}
}
- The app's window has a customized title, size (dimensions) and icon. More window customization are described here.
- An application-wide theme is applied that follow Material Design principles. Learn more about theming here.
remember
is used to create an internal state in the composable. Learn more about state management here.Column
is used to place items vertically on the screen. Learn about the available layouts here.- Modifier is used to customize the composable's appearance and behavior. For example,
Modifier.weight
is used to size the Column's children by dividing the available space proportionally. More about Modifiers here.
DisplayPanel
DisplayPanel
is the top component of the calculator.
@Composable
fun DisplayPanel(
modifier: Modifier,
mainOutput: MutableState<TextFieldValue>
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface)
.padding(CALCULATOR_PADDING)
.background(Color.White)
.border(color = Color.Gray, width = 1.dp)
.padding(start = 16.dp, end = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.End
) {
Text(
text = mainOutput.value.text,
style = TextStyle(
fontSize = 48.sp,
fontFamily = jostFontFamily
),
overflow = TextOverflow.Ellipsis,
softWrap = false,
maxLines = 1,
)
}
}
- We can find here a more complex usage of Modifiers.
- The calculator's output is displayed using the
Text
composable with custom styling and behavior. TextFieldValue
holds the output text's editing state. It was initialized in the app's entry point, and is modified by the keyboard keys.
Keyboard
This is the second component of the calculator placed below DisplayPanel
.
@Composable
fun Keyboard(
modifier: Modifier,
mainOutput: MutableState<TextFieldValue>
) {
Surface(modifier) {
KeyboardKeys(mainOutput)
}
}
@Composable
fun KeyboardKeys(mainOutput: MutableState<TextFieldValue>) {
Row(modifier = Modifier.fillMaxSize()) {
KeyboardLayout.forEach { keyColumn ->
Column(modifier = Modifier.weight(1f)) {
keyColumn.forEach { key ->
KeyboardKey(Modifier.weight(1f), key, mainOutput)
}
}
}
}
}
- the
Surface
composable will implicitly use the surface color defined in the app's Material theme. Row
is used to to place items horizontally on the screen.
@Composable
fun KeyboardKey(modifier: Modifier, key: Key?, mainOutput: MutableState<TextFieldValue>) {
if (key == null) {
return EmptyKeyView(modifier)
}
KeyView(modifier = modifier.padding(1.dp), onClick = key.onClick?.let {
{ it(mainOutput) }
} ?: {
val textValue = mainOutput.value.text.let {
if (it == "0") key.value else it + key.value
}
mainOutput.value = TextFieldValue(text = textValue)
}) {
if (key.icon == null) {
val textStyle = if (key.type == KeyType.COMMAND) {
TextStyle(
color = MaterialTheme.colors.primary,
fontSize = 22.sp
)
} else {
TextStyle(
fontFamily = jostFontFamily,
fontSize = 29.sp
)
}
Text(
text = key.value,
style = textStyle
)
} else {
Icon(
asset = key.icon,
tint = MaterialTheme.colors.primary
)
}
}
}
val KEY_BORDER_WIDTH = 1.dp
val KEY_BORDER_COLOR = Color.Gray
val KEY_ACTIVE_BACKGROUND = Color.White
@Composable
fun KeyView(
modifier: Modifier = Modifier,
onClick: () -> Unit,
children: @Composable ColumnScope.() -> Unit
) {
val active = remember { mutableStateOf(false) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier.fillMaxWidth()
.padding(CALCULATOR_PADDING)
.clickable(onClick = onClick)
.background(color = if (active.value) KEY_ACTIVE_BACKGROUND else MaterialTheme.colors.background)
.border(width = KEY_BORDER_WIDTH, color = KEY_BORDER_COLOR)
.pointerMoveFilter(
onEnter = {
active.value = true
false
},
onExit = {
active.value = false
false
}
),
children = children
)
}
@Composable
fun EmptyKeyView(modifier: Modifier) = Box(
modifier = modifier.fillMaxWidth()
.background(MaterialTheme.colors.background)
.border(width = KEY_BORDER_WIDTH, color = KEY_BORDER_COLOR)
)
Modifier.clickable
is used to make the keys receive click interactions from the user.Modifier.pointerMoveFilter
is used here to change the keys background color when hovering over them. Learn more about mouse events here.
Misc.
The following code was used to directly define a vector graphic (SVG aka vector drawable on Android) without using any XML file:
val OutlineBackspace = vectorBuilder().addPath(
pathData = parsePath("M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM22,19L7.07,19L2.4,12l4.66,-7L22,5v14zM10.41,17L14,13.41 17.59,17 19,15.59 15.41,12 19,8.41 17.59,7 14,10.59 10.41,7 9,8.41 12.59,12 9,15.59z"),
fill = SolidColor(Color.Blue)
).build()
private fun vectorBuilder() = VectorAssetBuilder(
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
)
private fun parsePath(pathStr: String) = PathParser().parsePathString(pathStr).toNodes()
The app's full source code is available on GitHub.
More resources and documentation
Here are some external resources and tutorials covering Compose aspects not presented in this article:
Compose for Desktop
- Image and in-app icons manipulations
- Keyboard events handling
- Scrollbar usage
- Tray and menu notification
- Compose for Desktop samples
Jetpack Compose
- Jetpack Compose basics tutorial
- Jetpack Compose basics codelab
- Layouts in Jetpack Compose codelab
- Jetpack Compose samples
- Jetpack Compose Playground: Community-driven collection of Jetpack Compose example code and tutorials