Kotlin Multiplatform: What You Can Only Do in desktopMain (with Code Examples)

Kotlin Multiplatform: What Can Only Be Done in desktopMain? 

Overview

In the world of cross-platform development, Kotlin Multiplatform (KMP) paired with Compose Multiplatform is a game-changer. But when targeting desktop apps (Windows, macOS, Linux), the desktopMain source set unlocks unique capabilities not available on mobile platforms like Android or iOS.

These features leverage desktop-specific APIs, such as window management, system trays, and Java libraries, enabling richer, more native experiences. This guide covers what can only be done in desktopMain, with usage examples and best practices for 2025.

Desktop-Specific Features Table

# Feature Usage & Reason / Notes
1 Set window size (WindowState) Use rememberWindowState(size = DpSize(width, height)) in Window composable. Controls initial or dynamic window dimensions; desktop apps need explicit sizing unlike mobile full-screen defaults.
2 Set window position Use rememberWindowState(position = WindowPosition(alignment)) in Window. Positions window on screen (e.g., center); desktop multi-window environment requires manual placement.
3 Change window title/icon dynamically Set window.title and window.icon in composable scope. Updates app branding at runtime; desktop OS window manager handles titles/icons.
4 System Tray (Tray) Use Tray(state, icon, menu) to add icon/menu to tray. Minimizes app to background with quick access; desktop OS tray for utilities/notifiers.
5 Menu Bar Use MenuBar { Menu(...) { Item(...) } } attached to window. Creates native top menus (File/Edit); standard desktop app navigation.
6 Context Menus Use Modifier.contextMenu { Item(...) } on composables. Right-click popups for actions; mouse-driven desktop interaction.
7 Tooltips Use TooltipArea(tooltip = { ... }) { content }. Hover text for guidance; relies on desktop mouse hover events.
8 Custom Dialogs Use Dialog(onCloseRequest) for modal windows. Separate alerts/inputs; desktop supports multiple overlapping windows.
9 Keyboard shortcuts/events Use Modifier.onKeyEvent { ... } with key checks. Handles shortcuts like Ctrl+S; desktop keyboard-focused input.
10 Mouse events (hover/scroll) Use Modifier.pointerHoverIcon or onPointerEvent(PointerEventType.Scroll). Hover effects, wheel scrolling; desktop pointer precision.
11 Native drag-and-drop Use Modifier.dragAndDropTarget { ... } for file drops. OS-level file/content dragging; no sandbox restrictions on desktop.
12 File picker (AWT/Swing) Use JFileChooser for native dialogs. Select/save files with OS picker; Java APIs for desktop integration.
13 Local file system access Use File(path).readText() or similar. Direct I/O without permissions; desktop lacks mobile sandboxes.
14 Java libraries (AWT/Swing) Import and use java.awt.*, javax.swing.*. Leverage Java ecosystem; unavailable on non-JVM platforms.
15 Desktop notifications Use TrayIcon.displayMessage via AWT. System alerts; Java APIs for tray-based notifications.
16 Undecorated windows Set undecorated = true in Window. No title bar for custom frames; desktop UI flexibility (e.g., games).
17 Transparent windows Set transparent = true in Window. Overlay effects with opacity; desktop compositing support.
18 Always on top Set window.isAlwaysOnTop = true. Keeps window above others; desktop layering for tools.
19 Minimize to tray Hide window on minimize, show tray icon. Space-saving for background apps; integrates with tray feature.
20 Multi-window support Create multiple Window composables. Open several app windows; desktop multitasking.
21 Custom scrollbars Override LocalScrollbarStyle with ScrollbarStyle. Theme thickness/color; desktop scrollbar customization.
22 Desktop accessibility (screen readers) Use Modifier.semantics { contentDescription = ... }. OS integration for voice-over; desktop accessibility APIs.

Version Compatibility & Dependencies

This implementation requires Kotlin 2.0+ and Compose Multiplatform 1.7.0 (as of 2025). Add the Compose Multiplatform dependencies to your project:


dependencies {
    implementation("org.jetbrains.compose.desktop:desktop:1.7.0")
    implementation("org.jetbrains.compose.ui:ui:1.7.0")
}
    

Ensure your project uses Compose Multiplatform for desktop targets. Refer to the Compose Multiplatform Guidelines for details.

Implementing Desktop-Specific Features in Compose Multiplatform

1. Set window size (WindowState)

Sets the initial size of the desktop window.


import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(size = DpSize(800.dp, 600.dp))
    ) {
        // Content
    }
}
    

Result: A desktop window opens with a fixed size of 800x600 dp.


2. Set window position (placement, e.g., center)

Sets the initial position of the desktop window.


import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.Alignment

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(position = WindowPosition(Alignment.Center))
    ) {
        // Content
    }
}
    

Result: The window opens centered on the screen.


3. Change window title and icon dynamically

Updates the window's title and icon at runtime.


import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        LaunchedEffect(Unit) {
            window.title = "New Title"
            window.icon = painterResource("icon.png")
        }
        // Content
    }
}
    

Result: The window title and icon are updated dynamically.


4. System Tray integration (Tray)

Adds an icon to the system tray with a menu.


import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.res.painterResource

fun main() = application {
    val trayState = rememberTrayState()
    Tray(
        state = trayState,
        icon = painterResource("icon.png"),
        menu = {
            Item("Exit") { exitApplication() }
        }
    )
    // Window or content
}
    

Result: An icon appears in the system tray with a menu.


5. Menu Bar (top-level menus)

Adds a native menu bar to the window.


import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        MenuBar {
            Menu("File") {
                Item("Open", onClick = { /* Handle */ })
                Item("Exit", onClick = ::exitApplication)
            }
        }
        // Content
    }
}
    

Result: A menu bar with File menu appears at the top.


6. Context Menus (right-click popups)

Adds a context menu to a composable.


import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Box(Modifier.contextMenu {
            Item("Copy") { /* Handle */ }
            Item("Paste") { /* Handle */ }
        })
    }
}
    

Result: Right-clicking shows a popup menu.


7. Tooltips

Adds hover tooltips to composables.


import androidx.compose.material.Text
import androidx.compose.material.TooltipArea
import androidx.compose.material.TooltipPlacement
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        TooltipArea(
            tooltip = { Text("Tooltip text") },
            modifier = Modifier,
            delayMillis = 500,
            tooltipPlacement = TooltipPlacement.CursorPoint()
        ) {
            Text("Hover me")
        }
    }
}
    

Result: Hovering shows a tooltip text.


8. Custom Dialogs (Dialog window)

Opens a custom dialog window.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        val showDialog = remember { mutableStateOf(false) }
        Button(onClick = { showDialog.value = true }) { Text("Show Dialog") }
        if (showDialog.value) {
            Dialog(onCloseRequest = { showDialog.value = false }) {
                Text("Dialog content")
            }
        }
    }
}
    

Result: Clicking the button opens a dialog window.


9. Keyboard shortcuts and events

Handles keyboard events and shortcuts.


import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.*
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Box(Modifier.onKeyEvent { event ->
            if (event.key == Key.Enter && event.isCtrlPressed) {
                // Handle Ctrl+Enter
                true
            } else false
        })
    }
}
    

Result: Pressing Ctrl+Enter triggers the handler.


10. Mouse events (hover, scroll wheel)

Handles mouse hover and scroll events.


import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Box(Modifier.pointerHoverIcon(PointerIcon.Hand)
            .onPointerEvent(PointerEventType.Scroll) { /* Handle wheel */ })
    }
}
    

Result: Hover changes cursor, scroll triggers handler.


11. Native drag-and-drop from OS

Handles drag-and-drop of files from OS.


import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.draganddrop.dragAndDropTarget
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Box(Modifier.fillMaxSize().dragAndDropTarget(
            shouldStartDrag = { true },
            drop = { dragInfo ->
                val files = dragInfo.transferData?.getNativeFileList()
                // Handle files
                true
            }
        ))
    }
}
    

Result: Dropping files triggers the handler.


12. File picker using AWT or Swing

Opens a native file picker.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Button(onClick = {
            val chooser = JFileChooser().apply {
                fileFilter = FileNameExtensionFilter("Images", "jpg", "png")
            }
            if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
                val file = chooser.selectedFile
                // Handle
            }
        }) {
            Text("Open File Picker")
        }
    }
}
    

Result: A native file picker opens.


13. Accessing local file system freely (no sandbox)

Reads a file from the local file system.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import java.io.File

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Button(onClick = {
            val file = File("path/to/file.txt")
            val content = file.readText()
            // Use content
        }) {
            Text("Read File")
        }
    }
}
    

Result: Reads the file content without permissions.


14. Use of java.awt, javax.swing, etc.

Uses Swing components in Compose.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import java.awt.Color
import java.awt.Graphics
import javax.swing.JPanel

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Button(onClick = {
            val panel = object : JPanel() {
                override fun paintComponent(g: Graphics) {
                    super.paintComponent(g)
                    g.color = Color.RED
                    g.fillRect(0, 0, width, height)
                }
            }
            // Use panel
        }) {
            Text("Use Swing")
        }
    }
}
    

Result: Creates a Swing panel with red fill.


15. Desktop notifications (AWT TrayIcon)

Displays a system notification.


import java.awt.SystemTray
import java.awt.TrayIcon
import java.awt.Toolkit
import androidx.compose.ui.window.application
import java.awt.image.BufferedImage

fun main() = application {
    if (SystemTray.isSupported()) {
        val tray = SystemTray.getSystemTray()
        val image: BufferedImage = Toolkit.getDefaultToolkit().createImage("icon.png") as BufferedImage
        val trayIcon = TrayIcon(image, "App")
        tray.add(trayIcon)
        trayIcon.displayMessage("Title", "Message", TrayIcon.MessageType.INFO)
    }
    // Content
}
    

Result: A system notification is displayed.


16. Undecorated windows (no title bar)

Creates a window without title bar.


import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        undecorated = true
    ) {
        // Content
    }
}
    

Result: Window opens without title bar.


17. Transparent windows

Creates a transparent window.


import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(
        onCloseRequest = ::exitApplication,
        transparent = true
    ) {
        // Content (use shapes or alpha for visibility)
    }
}
    

Result: Window is transparent.


18. Always on top windows

Keeps the window always on top.


import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        LaunchedEffect(Unit) {
            window.isAlwaysOnTop = true
        }
        // Content
    }
}
    

Result: Window stays on top of others.


19. Minimize to tray

Minimizes the window to tray.


import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.res.painterResource

fun main() = application {
    val trayState = rememberTrayState()
    Tray(
        state = trayState,
        icon = painterResource("icon.png")
    )
    Window(onCloseRequest = { window.isVisible = false }) {
        LaunchedEffect(Unit) {
            window.addWindowStateListener { event ->
                if (event.newState.isMinimized) {
                    window.isVisible = false
                }
            }
        }
        // Content
    }
}
    

Result: Minimizing hides window to tray.


20. Multi-window support

Supports multiple windows.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    val showSecondWindow = remember { mutableStateOf(false) }
    Window(onCloseRequest = ::exitApplication, title = "Main") {
        Button(onClick = { showSecondWindow.value = true }) { Text("Open Second Window") }
    }
    if (showSecondWindow.value) {
        Window(onCloseRequest = { showSecondWindow.value = false }, title = "Second") {
            Text("Second Window")
        }
    }
}
    

Result: Opens multiple windows.


21. Custom scrollbars

Customizes scrollbar styles.


import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.material.LocalScrollbarStyle
import androidx.compose.material.ScrollbarStyle

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        CompositionLocalProvider(
            LocalScrollbarStyle provides ScrollbarStyle(
                // Customize: minimalHeight, thickness, shape, hoverDurationMillis, etc.
                thickness = 8.dp,
                hoverColor = Color.Gray
            )
        ) {
            Box(Modifier.verticalScroll(ScrollState(0))) {
                // Scrollable content
            }
        }
    }
}
    

Result: Scrollbars are customized in thickness and color.


22. Desktop accessibility (screen readers)

Adds accessibility semantics.


import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        Text(
            "Accessible Text",
            modifier = Modifier.semantics {
                contentDescription = "This is accessible text for screen readers"
                role = Role.Heading
            }
        )
    }
}
    

Result: Screen readers read the content description.

Key Points for Implementation

  • desktopMain Source Set: Place all desktop-specific code here to avoid compilation issues on other platforms.
  • Expect/Actual: Use for shared abstractions with platform-specific implementations.
  • Window APIs: Control size, position, and decorations exclusively in desktopMain.
  • Java Integration: Leverage AWT/Swing for native dialogs and notifications.
  • Accessibility: Add semantics for screen readers; ensure keyboard navigation.

Theming and Customization

Customize desktop features using MaterialTheme or platform-specific styles.


import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.graphics.Color

fun main() = application {
    MaterialTheme(
        colorScheme = MaterialTheme.colorScheme.copy(
            primary = Color(0xFF4CAF50)
        )
    ) {
        Window(onCloseRequest = ::exitApplication) {
            // Your themed content here
        }
    }
}
    

FAQ

What is desktopMain in KMP?

A source set for desktop-specific code in Kotlin Multiplatform projects.


Why can't these features be used on mobile?

Mobile platforms have sandboxes and different UI paradigms, lacking direct access to desktop APIs like trays or undecorated windows.


How to handle platform differences?

Use expect/actual declarations for shared interfaces with desktop implementations.


What is the minimum Kotlin version required?

Kotlin 2.0+ with Compose Multiplatform 1.7.0.


Can I package desktop apps?

Yes, use Gradle for MSI, DMG, or DEB installers.

Post a Comment

Previous Post Next Post