Kotlin Multiplatform Development Help

Top-level windows management

Compose Multiplatform for desktop provides various features for managing windows. You can hide windows in the tray, make them draggable, adapt size, change position, and so on.

Open and close windows

You can use the Window() function to create a regular window. To put it in a composable scope, use Window() in the application entry point:

import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { Window(onCloseRequest = ::exitApplication) { // Content of the window } }

As a composable function, Window() allows you to change its properties declaratively. For example, you can open a window with one title and change the title later:

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.window.Window import androidx.compose.ui.window.application fun main() = application { var fileName by remember { mutableStateOf("Untitled") } Window(onCloseRequest = ::exitApplication, title = "$fileName - Editor") { Button(onClick = { fileName = "note.txt" }) { Text("Save") } } }
Window properties: change title

Add conditions

You can also open and close windows using simple if conditions. In the following code sample, the application window is automatically closed after completing a task:

import androidx.compose.material.Text import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.delay fun main() = application { var isPerformingTask by remember { mutableStateOf(true) } LaunchedEffect(Unit) { // Do some heavy lifting delay(2000) isPerformingTask = false } if (isPerformingTask) { Window( onCloseRequest = ::exitApplication, title = "Window 1" ) { Text("Performing some tasks. Please wait!") } } else { Window( onCloseRequest = ::exitApplication, title = "Window 2" ) { Text("Hello, World!") } } }
Windows with conditions

If you want to use custom logic on application exit, such as showing a dialog, you can override the close action using the onCloseRequest callback. In the following code sample, instead of an imperative approach (window.close()), we use a declarative approach and close the window in response to the state change (isOpen = false).

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.window.DialogWindow import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { var isOpen by remember { mutableStateOf(true) } var isAskingToClose by remember { mutableStateOf(false) } if (isOpen) { Window( onCloseRequest = { isAskingToClose = true }, title = "Important document" ) { if (isAskingToClose) { DialogWindow( onCloseRequest = { isAskingToClose = false }, title = "Close without saving?" ) { Button( onClick = { isOpen = false } ) { Text("Yes") } } } } } }
Close with confirmation

Work with multiple windows

If an application has multiple windows, you can create a separate class for the application state and open or close windows in response to the mutableStateListOf changes:

import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { val applicationState = remember { MyApplicationState() } for (window in applicationState.windows) { key(window) { MyWindow(window) } } } @Composable private fun MyWindow( state: MyWindowState ) = Window(onCloseRequest = state::close, title = state.title) { MenuBar { Menu("File") { Item("New window", onClick = state.openNewWindow) Item("Exit", onClick = state.exit) } } } private class MyApplicationState { val windows = mutableStateListOf<MyWindowState>() init { windows += MyWindowState("Initial window") } fun openNewWindow() { windows += MyWindowState("Window ${windows.size}") } fun exit() { windows.clear() } private fun MyWindowState( title: String ) = MyWindowState( title, openNewWindow = ::openNewWindow, exit = ::exit, windows::remove ) } private class MyWindowState( val title: String, val openNewWindow: () -> Unit, val exit: () -> Unit, private val close: (MyWindowState) -> Unit ) { fun close() = close(this) }
Multiple windows

For a more complex example, see the notepad sample.

Minimize a window to the system tray

To hide the window instead of closing it, you can change the windowState.isVisible state:

import androidx.compose.material.Text import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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 kotlinx.coroutines.delay fun main() = application { var isVisible by remember { mutableStateOf(true) } Window( onCloseRequest = { isVisible = false }, visible = isVisible, title = "Counter", ) { var counter by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { counter++ delay(1000) } } Text(counter.toString()) } if (!isVisible) { Tray( TrayIcon, tooltip = "Counter", onAction = { isVisible = true }, menu = { Item("Exit", onClick = ::exitApplication) }, ) } } object TrayIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }
Hide instead of closing

singleWindowApplication() function

You can create a single window application by calling the singleWindowApplication() function.

The singleWindowApplication() function is easier to use but has the following limitations:

  • The application can have only one window.

  • You cannot add custom closing logic.

  • You cannot change the attributes of the window in runtime.

import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication { // Content of the window }

As an alternative, you can use the Window() composable in the application entry point.

Adaptive window size

When you don't know the size of the expected content and cannot specify the optimal window dimensions in advance, you can set one or both dimensions of WindowSize to Dp.Unspecified. Compose Multiplatform for desktop will automatically adjust the initial size of your window to fit the content:

import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState fun main() = application { Window( onCloseRequest = ::exitApplication, state = rememberWindowState(width = Dp.Unspecified, height = Dp.Unspecified), title = "Adaptive size", resizable = false ) { Column(Modifier.background(Color(0xFFEEEEEE))) { Row { Text("label 1", Modifier.size(100.dp, 100.dp).padding(10.dp).background(Color.White)) Text("label 2", Modifier.size(150.dp, 200.dp).padding(5.dp).background(Color.White)) Text("label 3", Modifier.size(200.dp, 300.dp).padding(25.dp).background(Color.White)) } } } }
Adaptive window size

Changing the window state

WindowState is a separate API class for the window placement, current position, and size. The placement attribute allows you to specify how the window is placed on the screen: floating, maximized/minimized, or fullscreen. Any change of the state triggers automatic recomposition. To change the window state, use callbacks or observe it in composables:

import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.material.Checkbox import androidx.compose.material.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState fun main() = application { val state = rememberWindowState(placement = WindowPlacement.Maximized) Window(onCloseRequest = ::exitApplication, state, title = "Window state") { Column { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( state.placement == WindowPlacement.Fullscreen, { state.placement = if (it) { WindowPlacement.Fullscreen } else { WindowPlacement.Floating } } ) Text("isFullscreen") } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( state.placement == WindowPlacement.Maximized, { state.placement = if (it) { WindowPlacement.Maximized } else { WindowPlacement.Floating } } ) Text("isMaximized") } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(state.isMinimized, { state.isMinimized = !state.isMinimized }) Text("isMinimized") } Text( "Position ${state.position}", Modifier.clickable { val position = state.position if (position is WindowPosition.Absolute) { state.position = position.copy(x = state.position.x + 10.dp) } } ) Text( "Size ${state.size}", Modifier.clickable { state.size = state.size.copy(width = state.size.width + 10.dp) } ) } } }
Changing the state

Listen to the window state

If you need to react to the state changes and send a value to another non-composable application level, for example, write it to the database, you can use the snapshotFlow() function. This function captures the current value of a composable's state.

import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.unit.DpSize import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach fun main() = application { val state = rememberWindowState() Window(onCloseRequest = ::exitApplication, state) { LaunchedEffect(state) { snapshotFlow { state.size } .onEach(::onWindowResize) .launchIn(this) snapshotFlow { state.position } .filter { it.isSpecified } .onEach(::onWindowRelocate) .launchIn(this) } } } private fun onWindowResize(size: DpSize) { println("onWindowResize $size") } private fun onWindowRelocate(position: WindowPosition) { println("onWindowRelocate $position") }

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.

The following code sample demonstrates how to use these composables to combine regular and modal windows:

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)) ) { // Content of the window } } } }

Draggable window area

To add a custom draggable title bar to the undecorated window or make the whole window draggable, you can use the WindowDraggableArea() composable:

import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application fun main() = application { Window(onCloseRequest = ::exitApplication, undecorated = true) { WindowDraggableArea { Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray)) } } }

WindowDraggableArea() can be used inside the singleWindowApplication(), Window(), and DialogWindow() composables only. To call it in another composable function, use a WindowScope as a receiver scope:

import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowScope import androidx.compose.ui.window.application fun main() = application { Window(onCloseRequest = ::exitApplication, undecorated = true) { AppWindowTitleBar() } } @Composable private fun WindowScope.AppWindowTitleBar() = WindowDraggableArea { Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray)) }
Draggable area

Transparent windows and other customizations

To create a transparent window, pass two parameters to the Window() function: transparent=true and undecorated=true. The window must be undecorated because it is impossible to decorate a transparent window.

The following code sample demonstrates how to combine composables to create a transparent window with rounded corners:

import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Surface import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.material.Text import androidx.compose.runtime.* fun main() = application { var isOpen by remember { mutableStateOf(true) } if (isOpen) { Window( onCloseRequest = { isOpen = false }, title = "Transparent Window Example", transparent = true, // Transparent window must be undecorated undecorated = true, ) { Surface( modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), color = Color.Transparent, // Window with rounded corners shape = RoundedCornerShape(20.dp) ) { Text("Hello World!", color = Color.White) } } } }

Swing interoperability

Compose Multiplatform for desktop uses Swing under the hood, so you can create a window using Swing directly:

import androidx.compose.ui.awt.ComposeWindow import java.awt.Dimension import javax.swing.JFrame import javax.swing.SwingUtilities fun main() = SwingUtilities.invokeLater { ComposeWindow().apply { size = Dimension(300, 300) defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE setContent { // Content of the window } isVisible = true } }

You can also use the scope of a Window() composable. In the following code sample, window is a ComposeWindow created inside Window():

import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.window.singleWindowApplication import java.awt.datatransfer.DataFlavor import java.awt.dnd.DnDConstants import java.awt.dnd.DropTarget import java.awt.dnd.DropTargetAdapter import java.awt.dnd.DropTargetDropEvent fun main() = singleWindowApplication { LaunchedEffect(Unit) { window.dropTarget = DropTarget().apply { addDropTargetListener(object : DropTargetAdapter() { override fun drop(event: DropTargetDropEvent) { event.acceptDrop(DnDConstants.ACTION_COPY) val fileName = event.transferable.getTransferData(DataFlavor.javaFileListFlavor) println(fileName) } }) } } }

If you need to use a dialog implemented in Swing, you can wrap it into a composable function:

import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.window.AwtWindow import androidx.compose.ui.window.application import java.awt.FileDialog import java.awt.Frame fun main() = application { var isOpen by remember { mutableStateOf(true) } if (isOpen) { FileDialog( onCloseRequest = { isOpen = false println("Result $it") } ) } } @Composable private fun FileDialog( parent: Frame? = null, onCloseRequest: (result: String?) -> Unit ) = AwtWindow( create = { object : FileDialog(parent, "Choose a file", LOAD) { override fun setVisible(value: Boolean) { super.setVisible(value) if (value) { onCloseRequest(file) } } } }, dispose = FileDialog::dispose )

What's next

Explore the tutorials about other desktop components.

Last modified: 01 October 2024