Skip to main content

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

1

Install JDK

Install JDK 11 or higher:
# Using Homebrew
brew install openjdk@21
2

Verify Java Installation

Check Java version:
java -version
Should show Java 11 or higher.
3

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:
jvmMain/.../Main.kt
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

Package for All Platforms

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/

Platform-Specific Builds

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")
        }
    }
}

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:
jvmMain/.../Main.kt
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):
jvmMain/.../Main.kt
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:
jvmMain/.../Main.kt
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")
        }
    }
}

Performance Optimization

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

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.
Error: DMG creation failsSolution: Install Xcode command-line tools:
xcode-select --install
Error: WiX Toolset not foundSolution:
  1. Install WiX Toolset v3
  2. Add WiX to PATH
  3. Restart terminal/IDE
Error: Window opens but shows blank/black screenSolution: Check for rendering issues:
// Add hardware acceleration flag
System.setProperty("sun.java2d.opengl", "true")
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

Build docs developers (and LLMs) love