Custom Title Bar, Skinning, and ViewModel in Kotlin Compose Desktop

This article demonstrates how to implement a custom title bar with window controls, apply dynamic skinning, integrate ViewModel using Precompose, handle network and file I/O, manage multi‑page navigation, open file selectors, and use KV storage in a Kotlin Compose Multiplatform desktop application, with full code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Custom Title Bar, Skinning, and ViewModel in Kotlin Compose Desktop
Demo effect
Demo effect

1. Introduction

The previous article introduced the basics of Kotlin + Compose + Multiplatform cross‑platform development, including project setup, configuration, packaging, and basic layout. This second part dives into desktop‑specific implementations.

The topics covered in this article are:

1. Custom title bar with drag, maximize, minimize, etc.

2. Skinning (theme switching).

3. Using ViewModel on the desktop similar to Android.

4. Network requests and file I/O.

5. Multi‑page navigation similar to TabHost and routing.

6. Opening a file chooser.

7. KV storage usage.

Source code repository: https://github.com/wgllss/Kotlin_Compose_Multiplatform_Demo

2. Custom Title Bar, Drag, Maximize, Minimize

To hide the default window title bar, set Window.undecorated = true. Define the minimum window size with:

window.minimumSize = Dimension(JvmConfig.windowWidth, JvmConfig.windowHeight)

Control window state:

Maximize: window.placement = WindowPlacement.Maximized Restore: window.placement = WindowPlacement.Floating Minimize: windowState.isMinimized = true Close: ApplicationScope.exitApplication() Make the custom title bar draggable:

scope.WindowDraggableArea(Modifier.fillMaxWidth().height(40.dp)) {
    // custom title bar content
}

3. Skinning (Theme Switching)

Apply a unified UI theme with:

MaterialTheme(colorScheme = ThemeManager.skinTheme) {
    Surface(color = MaterialTheme.colorScheme.background) {
        // content
    }
}

Switch themes by updating a mutableStateOf value:

var skinTheme by mutableStateOf(getColorsSchemes(skinThemeType))

Define multiple color schemes and store the selected index using KV storage:

object ThemeManager {
    var skinThemeType by mutableStateOf(PlatformKVStore.getSkinType())
    var skinTheme by mutableStateOf(getColorsSchemes(skinThemeType))
    private fun getColorsSchemes(skinType: Int) = when (skinType) {
        0 -> LightColors
        1 -> DarkColors
        2 -> Colors72
        // ... other schemes ...
        else -> LightColors
    }
}

When the user selects a new skin, update the state and persist the index:

PlatformKVStore.saveSkin(it)
ThemeManager.skinTheme = item

4. Using ViewModel on Desktop

Add the Precompose libraries:

jvmMain.dependencies {
    // Desktop ViewModel support
    implementation("moe.tlaster:precompose:1.6.2") // >=1.6.0
    implementation("moe.tlaster:precompose-viewmodel:1.6.2") // submodule
}

Features include Windows/macOS support, deep integration with ViewModel and MVI, StateFlow composition, lifecycle compatibility, desktop‑specific lifecycle events, and coroutine support for shared desktop and mobile codebases.

5. Network and File I/O

Since the desktop target runs on the JVM, you can reuse the Android networking stack. Use Okhttp + Retrofit for HTTP requests, and standard Java/Kotlin I/O APIs for file operations.

6. Multi‑Page Navigation (TabHost & Routing)

Compose provides Tab + VerticalPager/HorizontalPager for simple tab navigation, or you can use a full router with Precompose:

@Composable
fun NavGraph() {
    val navigator = rememberNavigator("key222222")
    val viewModel = viewModel { TabViewModel9() }
    NavHost(
        navigator = navigator,
        navTransition = remember {
            NavTransition(
                createTransition = fadeIn(),
                destroyTransition = fadeOut(),
                pauseTransition = fadeOut(),
                resumeTransition = fadeIn()
            )
        },
        initialRoute = RouterUrls.news_first
    ) {
        scene(RouterUrls.news_first) {
            RouterFirst("BA8D4A3Rwangning", viewModel) {
                navigator.navigate("${RouterUrls.news_second}?title=${it.title}&docid=${it.docid}")
            }
        }
        scene(RouterUrls.news_second) { backStackEntry ->
            val title = backStackEntry.query<String>("title") ?: ""
            val docid = backStackEntry.query<String>("docid") ?: ""
            RouterSample(navigator, docid, title)
        }
    }
}

7. Opening a File Chooser

Use the native Swing JFileChooser component. Example for selecting a directory:

JFileChooser(PlatformKVStore.getDownloadDir()).apply {
    fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
    if (showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
        val selectedDir = selectedFile.absolutePath
        _downloadPath.value = selectedDir
        PlatformKVStore.saveDownloadDir(selectedDir)
    }
}

Other selection modes: JFileChooser.FILES_ONLY (files only) and JFileChooser.FILES_AND_DIRECTORIES (both).

8. KV Storage Usage

Compose desktop can use com.russhwolf:multiplatform-settings:1.2.0 for key‑value storage across Android, iOS, macOS, JS, JVM, and Windows:

val settings = Settings()
settings.putInt("count", 5) // store integer
val value = settings.getInt("count", 0) // read with default

9. Summary

This tutorial covered custom title bar implementation, theme switching, desktop ViewModel integration, network and file I/O, multi‑page navigation, file chooser usage, and KV storage in a Kotlin Compose Multiplatform desktop application. The full source code is available on GitHub.

UIViewModelKotlinDesktopmultiplatformCompose
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.