Multi-Window Chat App in Compose Desktop | Kotlin Desktop #13
Video: Multi-Window Chat App in Compose Desktop | Kotlin Desktop #13 by Taught by Celeste AI - AI Coding Coach
Watch full page →Multi-Window Chat App in Compose Desktop with Kotlin
Discover how to build a multi-window chat application using Kotlin Compose Desktop by leveraging the application composable to manage multiple windows. This approach demonstrates dynamic window creation, shared state management across windows, and integration of system tray icons for enhanced desktop experience.
Code
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import java.awt.image.BufferedImage
import java.awt.Graphics2D
import java.awt.Color
import javax.swing.SwingUtilities
// Data classes for chat
data class Contact(val id: Int, val name: String)
data class Message(val sender: String, val content: String)
// Shared chat state with mutableStateListOf for reactive updates
class ChatState {
val contacts = listOf(
Contact(1, "Alice"),
Contact(2, "Bob"),
Contact(3, "Carol")
)
val openChats = mutableStateListOf()
val messages = mutableStateMapOf>().apply {
contacts.forEach { put(it.id, mutableListOf()) }
}
}
@Composable
fun ContactList(chatState: ChatState, onOpenChat: (Contact) -> Unit) {
LazyColumn(modifier = Modifier.fillMaxHeight().width(200.dp).padding(8.dp)) {
items(chatState.contacts) { contact ->
ListItem(
modifier = Modifier.clickable { onOpenChat(contact) },
text = { Text(contact.name) }
)
Divider()
}
}
}
@Composable
fun ChatWindow(contact: Contact, chatState: ChatState, onClose: () -> Unit) {
var input by remember { mutableStateOf("") }
val messages = chatState.messages[contact.id] ?: emptyList()
Window(
onCloseRequest = onClose,
title = "Chat with ${contact.name}",
state = rememberWindowState(width = 400.dp, height = 300.dp)
) {
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(messages) { message ->
Text("${message.sender}: ${message.content}")
}
}
Row {
TextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { Text("Type a message") },
// Send message on Enter key press
keyboardActions = KeyboardActions(onSend = {
if (input.isNotBlank()) {
chatState.messages[contact.id]?.add(Message("You", input))
input = ""
}
})
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
if (input.isNotBlank()) {
chatState.messages[contact.id]?.add(Message("You", input))
input = ""
}
}) {
Text("Send")
}
}
}
}
}
fun createTrayIconImage(): BufferedImage {
val size = 16
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val g: Graphics2D = image.createGraphics()
g.color = Color(0, 120, 215)
g.fillOval(0, 0, size, size)
g.dispose()
return image
}
@Composable
@Preview
fun App() {
val chatState = remember { ChatState() }
val trayState = rememberTrayState()
val openChats = chatState.openChats
application {
// System tray icon with menu to quit
Tray(
state = trayState,
icon = createTrayIconImage(),
tooltip = "Multi-Window Chat",
menu = {
Item("Quit", onClick = ::exitApplication)
}
)
// Main window showing contact list
Window(
onCloseRequest = ::exitApplication,
title = "Contacts",
state = rememberWindowState(width = 220.dp, height = 400.dp)
) {
ContactList(chatState) { contact ->
if (!openChats.contains(contact)) {
openChats.add(contact)
}
}
}
// Dynamically create chat windows for each open chat
for (contact in openChats) {
key(contact.id) {
ChatWindow(contact, chatState) {
openChats.remove(contact)
}
}
}
}
}
Key Points
- The
applicationcomposable manages the app lifecycle and allows multipleWindowcomposables to create multiple windows. - Dynamic windows are created by iterating over a mutable state list (
mutableStateListOf) and usingkey()for stable identity tracking. - Shared state across windows is maintained with
mutableStateListOfandmutableStateMapOfto keep messages and open chats reactive. - The system tray icon is created programmatically with Java 2D and integrated using the
Traycomposable for desktop convenience. rememberWindowStatecontrols window size and position, while keyboard actions andTextFieldhandle Enter-to-send message input.