Desktop Platform
Build and deploy GreenhouseAdmin as a native desktop application using Compose Multiplatform for Desktop (JVM). This guide covers running the desktop app, building native distributions, and desktop-specific features.
Overview
The Desktop platform uses:
Target : JVM (Java Virtual Machine)
JVM Version : Java 11
UI Framework : Compose Multiplatform for Desktop
HTTP Engine : OkHttp (Ktor client)
Distribution Formats : DMG (macOS), MSI (Windows), DEB (Linux)
The desktop app shares the same Compose UI code as other platforms while running on the JVM.
Prerequisites
Install JDK
Install JDK 11 or higher: # Using Homebrew
brew install openjdk@21
Download from Oracle or Adoptium # Ubuntu/Debian
sudo apt install openjdk-21-jdk
# Fedora
sudo dnf install java-21-openjdk
Verify Java Installation
Check Java version: Should show Java 11 or higher.
Set JAVA_HOME (if needed)
export JAVA_HOME = $( /usr/libexec/java_home -v 21 )
Configuration
Desktop-Specific Dependencies
The desktop platform uses OkHttp for networking and Swing coroutines:
composeApp/build.gradle.kts
jvmMain. dependencies {
implementation (compose.desktop.currentOs)
implementation (libs.kotlinx.coroutinesSwing)
// Ktor - OkHttp engine for JVM
implementation (libs.ktor.client.okhttp)
}
Application Configuration
Configure desktop application properties:
composeApp/build.gradle.kts
compose. desktop {
application {
mainClass = "com.apptolast.greenhouse.admin.MainKt"
nativeDistributions {
targetFormats (TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.apptolast.greenhouse.admin"
packageVersion = "1.0.0"
}
}
}
Dependency Injection (Koin)
Desktop requires platform-specific initialization in the main function:
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main () = application {
initKoin () // Initialize Koin
Window (
onCloseRequest = :: exitApplication ,
title = "Greenhouse Admin"
) {
App () // Shared Compose UI from commonMain
}
}
Development
Running the Desktop App
Run the desktop application directly:
./gradlew :composeApp:run
The application window will launch immediately.
Hot Reload
For faster development, use hot reload:
./gradlew :composeApp:run --continuous
The app will automatically rebuild and restart when you modify Kotlin files.
Hot reload works best with smaller code changes. Large refactors may require a full restart.
Building Native Distributions
Build distribution packages for all configured platforms:
./gradlew :composeApp:packageDistributionForCurrentOS
This creates platform-specific packages:
macOS : DMG installer
Windows : MSI installer
Linux : DEB package
Output Location : composeApp/build/compose/binaries/main/
macOS (DMG)
Windows (MSI)
Linux (DEB)
Build a DMG installer for macOS: ./gradlew :composeApp:packageDmg
Output : composeApp/build/compose/binaries/main/dmg/DMG creation requires macOS. For code signing, configure in build.gradle.kts: nativeDistributions {
macOS {
bundleID = "com.apptolast.greenhouse.admin"
signing {
sign. set ( true )
identity. set ( "Developer ID Application: Your Name" )
}
}
}
Build an MSI installer for Windows: ./gradlew :composeApp:packageMsi
Output : composeApp/build/compose/binaries/main/msi/MSI creation requires Windows and WiX Toolset v3.
Download from wixtoolset.org Build a DEB package for Debian/Ubuntu: ./gradlew :composeApp:packageDeb
Output : composeApp/build/compose/binaries/main/deb/Install the package: sudo dpkg -i greenhouse-admin_1.0.0_amd64.deb
Executable JAR
Build a runnable JAR file:
./gradlew :composeApp:packageUberJarForCurrentOS
Output : composeApp/build/compose/jars/
Run the JAR:
java -jar composeApp-1.0.0.jar
The uber JAR includes all dependencies and can be 50-100MB in size.
Desktop-Specific Features
Window Configuration
Customize the desktop window:
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import androidx.compose.ui.unit.dp
fun main () = application {
Window (
onCloseRequest = :: exitApplication ,
title = "Greenhouse Admin" ,
state = WindowState (
width = 1280 .dp,
height = 800 .dp
),
resizable = true ,
icon = painterResource ( "app_icon.png" )
) {
App ()
}
}
Add native menu bar (macOS/Windows):
import androidx.compose.ui.window.MenuBar
fun main () = application {
Window (
onCloseRequest = :: exitApplication ,
title = "Greenhouse Admin"
) {
MenuBar {
Menu ( "File" ) {
Item ( "Open" , onClick = { /* ... */ })
Item ( "Save" , onClick = { /* ... */ })
Separator ()
Item ( "Exit" , onClick = { exitApplication () })
}
Menu ( "Edit" ) {
Item ( "Cut" , onClick = { /* ... */ })
Item ( "Copy" , onClick = { /* ... */ })
Item ( "Paste" , onClick = { /* ... */ })
}
}
App ()
}
}
System Tray
Add system tray icon:
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.TrayState
fun main () = application {
var isVisible by remember { mutableStateOf ( true ) }
Tray (
icon = painterResource ( "tray_icon.png" ),
state = TrayState (),
tooltip = "Greenhouse Admin" ,
menu = {
Item ( "Show" , onClick = { isVisible = true })
Item ( "Exit" , onClick = :: exitApplication )
}
)
if (isVisible) {
Window (
onCloseRequest = { isVisible = false },
title = "Greenhouse Admin"
) {
App ()
}
}
}
File Dialogs
Use native file dialogs:
import androidx.compose.ui.window.AwtWindow
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@Composable
fun FileChooser (
onFileSelected: ( File ?) -> Unit
) {
AwtWindow (
create = {
object : FileDialog ( null as Frame?, "Choose a file" , LOAD) {
override fun setVisible (visible: Boolean ) {
super . setVisible (visible)
if (visible) {
val file = if (directory != null && file != null ) {
File (directory, file)
} else null
onFileSelected (file)
}
}
}
},
dispose = FileDialog:: dispose
)
}
Notifications
Show system notifications:
jvmMain/.../Notifications.kt
import androidx.compose.ui.window.Notification
import androidx.compose.ui.window.TrayState
fun showNotification (trayState: TrayState , message: String ) {
trayState. sendNotification (
Notification (
title = "Greenhouse Admin" ,
message = message,
type = Notification.Type.Info
)
)
}
Distribution Configuration
Advanced Package Options
Customize distribution packages:
composeApp/build.gradle.kts
compose. desktop {
application {
nativeDistributions {
targetFormats (TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "GreenhouseAdmin"
packageVersion = "1.0.0"
description = "Greenhouse client management portal"
copyright = "© 2024 AppToLast. All rights reserved."
vendor = "AppToLast"
// Application icon
macOS {
iconFile. set (project. file ( "icons/app_icon.icns" ))
}
windows {
iconFile. set (project. file ( "icons/app_icon.ico" ))
menuGroup = "AppToLast"
upgradeUuid = "YOUR-UUID-HERE"
}
linux {
iconFile. set (project. file ( "icons/app_icon.png" ))
}
// Runtime modules (reduces package size)
modules (
"java.base" ,
"java.desktop" ,
"java.naming" ,
"java.sql" ,
"jdk.unsupported"
)
}
}
}
Code Signing (macOS)
Sign and notarize macOS builds:
composeApp/build.gradle.kts
nativeDistributions {
macOS {
bundleID = "com.apptolast.greenhouse.admin"
signing {
sign. set ( true )
identity. set ( "Developer ID Application: Your Name (TEAM_ID)" )
}
notarization {
appleID. set ( "[email protected] " )
password. set ( "@keychain:AC_PASSWORD" )
ascProvider. set ( "TEAM_ID" )
}
}
}
JVM Arguments
Optimize JVM performance:
composeApp/build.gradle.kts
compose. desktop {
application {
jvmArgs (
"-Xmx2G" ,
"-Xms512M" ,
"-XX:+UseG1GC" ,
"-Dfile.encoding=UTF-8"
)
}
}
Resource Embedding
Embed resources in the JAR:
// Access embedded resources
val imageStream = this :: class .java. getResourceAsStream ( "/icons/logo.png" )
val image = imageStream?. use { ImageIO. read (it) }
Troubleshooting
UnsupportedClassVersionError
Error : UnsupportedClassVersionError: compiled with Java XXSolution : Ensure you’re using Java 11 or higher:java -version
./gradlew --version
Set JAVA_HOME to the correct JDK version.
Package creation fails on macOS
Error : DMG creation failsSolution : Install Xcode command-line tools:
MSI creation fails on Windows
Error : WiX Toolset not foundSolution :
Install WiX Toolset v3
Add WiX to PATH
Restart terminal/IDE
Application window is blank
Error : Window opens but shows blank/black screenSolution : Check for rendering issues:// Add hardware acceleration flag
System. setProperty ( "sun.java2d.opengl" , "true" )
OkHttp SSL errors on corporate networks
Error : SSL handshake failuresSolution : Add corporate CA certificates to Java keystore or configure Ktor to trust custom certificates.
Next Steps
Web Platform Deploy as a web application
Android Platform Build and deploy to Android devices
iOS Platform Build and deploy to iOS devices
Multiplatform Platform-specific implementations