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.
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 = item4. 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 default9. 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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
