Kotlin Multiplatform Development Help

Overview

You can use Compose Multiplatform to create macOS, Linux, and Windows desktop applications. This page gives a short overview of the desktop-specific components and events. Each section includes a link to a detailed tutorial.

Components

Windows and dialogs

You can use the Window composable to create a regular window and the DialogWindow composable for a modal window that locks its parent until the user closes the modal window:

import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberDialogState fun main() = application { Window( onCloseRequest = ::exitApplication, ) { var isDialogOpen by remember { mutableStateOf(false) } Button(onClick = { isDialogOpen = true }) { Text(text = "Open dialog") } if (isDialogOpen) { DialogWindow( onCloseRequest = { isDialogOpen = false }, state = rememberDialogState(position = WindowPosition(Alignment.Center)) ) { // Dialog's content } } } }

Compose Multiplatform provides various features for windows. You can adapt the window size, change its state (size, position), hide it in the tray, make the window draggable, transparent, and so on.

For more information, see the Top-level window management tutorial.

Context menus

Context menus are supported by default for the TextField composable (and the Text composable, if it's selectable):

import androidx.compose.foundation.ContextMenuDataProvider import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } Column { ContextMenuDataProvider( items = { listOf( ContextMenuItem("User-defined Action") {/*do something here*/ }, ContextMenuItem("Another user-defined action") {/*do something else*/ } ) } ) { TextField( value = text.value, onValueChange = { text.value = it }, label = { Text(text = "Input") } ) Spacer(Modifier.height(16.dp)) SelectionContainer { Text("Hello World!") } } } }

Default context menu options include copy, cut, paste, and select all. You can add more menu items, customize style and texts, and so on.

For more information, see the Context menu in Compose Multiplatform tutorial.

The system tray

You can use the Tray composable to send notifications to users in the system tray:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberNotification import androidx.compose.ui.window.rememberTrayState fun main() = application { var count by remember { mutableStateOf(0) } var isOpen by remember { mutableStateOf(true) } if (isOpen) { val trayState = rememberTrayState() val notification = rememberNotification("Notification", "Message from MyApp!") Tray( state = trayState, icon = TrayIcon, menu = { Item( "Increment value", onClick = { count++ } ) Item( "Send notification", onClick = { trayState.sendNotification(notification) } ) Item( "Exit", onClick = { isOpen = false } ) } ) Window( onCloseRequest = { isOpen = false }, icon = MyAppIcon ) { // Content: Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text(text = "Value: $count") } } } } object MyAppIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height)) drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f)) drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f)) } } object TrayIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }

There are three types of notifications:

  • notify, a simple notification.

  • warn, a warning notification.

  • error, an error notification.

You can also add an application icon to the system tray.

For more information, see the Menu, tray, and notifications tutorial.

You can use the MenuBar composable to create and customize the menu bar for a particular window:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyShortcut import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @OptIn(ExperimentalComposeUiApi::class) fun main() = application { var action by remember { mutableStateOf("Last action: None") } var isOpen by remember { mutableStateOf(true) } if (isOpen) { var isSubmenuShowing by remember { mutableStateOf(false) } Window(onCloseRequest = { isOpen = false }) { MenuBar { Menu("File", mnemonic = 'F') { Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true)) Item( "Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true) ) } Menu("Actions", mnemonic = 'A') { CheckboxItem( "Advanced settings", checked = isSubmenuShowing, onCheckedChange = { isSubmenuShowing = !isSubmenuShowing } ) if (isSubmenuShowing) { Menu("Settings") { Item("Setting 1", onClick = { action = "Last action: Setting 1" }) Item("Setting 2", onClick = { action = "Last action: Setting 2" }) } } Separator() Item("About", icon = AboutIcon, onClick = { action = "Last action: About" }) Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E') } } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text(text = action) } } } } object AboutIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }

For more information, see the Menu, tray, and notifications tutorial.

Events

Keyboard event handlers

You can set up keyboard event handlers with the onKeyEvent and onPreviewKeyEvent properties. Use onPreviewKeyEvent to define shortcuts because it guarantees that children components do not consume key events.

For example, here is how to set up an event handler for the active element in focus, the TextField composable:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication @OptIn(ExperimentalComposeUiApi::class) fun main() = singleWindowApplication { MaterialTheme { var consumedText by remember { mutableStateOf(0) } var text by remember { mutableStateOf("") } Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Text("Consumed text: $consumedText") TextField( value = text, onValueChange = { text = it }, modifier = Modifier.onPreviewKeyEvent { when { (it.isCtrlPressed && it.key == Key.Minus && it.type == KeyEventType.KeyUp) -> { consumedText -= text.length text = "" true } (it.isCtrlPressed && it.key == Key.Equals && it.type == KeyEventType.KeyUp) -> { consumedText += text.length text = "" true } else -> false } } ) } } }

You can also define keyboard event handlers that are always active in the current window for the Window, singleWindowApplication, and Dialog composables.

For more information, see the Keyboard event handling tutorial.

Tabbing navigation between components

You can set up navigation between components with the Tab keyboard shortcut for the next component and ⇧ + Tab for the previous one.

By default, the tabbed navigation allows you to move between focusable components in the order of their appearance. Focusable components include TextField, OutlinedTextField, and BasicTextField composables, as well as components that use Modifier.clickable, such as Button, IconButton, and MenuItem.

For example, here's a window where users can navigate between five text fields using standard shortcuts:

import androidx.compose.ui.window.application import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Spacer import androidx.compose.material.OutlinedTextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp fun main() = application { Window( state = WindowState(size = DpSize(350.dp, 500.dp)), onCloseRequest = ::exitApplication ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp) ) { for (x in 1..5) { val text = remember { mutableStateOf("") } OutlinedTextField( value = text.value, singleLine = true, onValueChange = { text.value = it } ) Spacer(modifier = Modifier.height(20.dp)) } } } } }

You can also make a non-focusable component focusable, customize the order of tabbing navigation, and put components into focus.

For more information, see the Tabbing navigation and keyboard focus tutorial.

What's next

Last modified: 19 September 2024