Compose Multiplatform da JetBrains levou o modelo declarativo do Jetpack Compose para alem do Android. Com ele, voce cria aplicativos desktop nativos para Windows, macOS e Linux usando Kotlin e os mesmos conceitos de UI que ja domina no Android. Neste guia, vamos cobrir tudo que voce precisa para comecar a desenvolver aplicativos desktop com Compose Multiplatform.
O que e Compose Multiplatform?
Compose Multiplatform e um framework de UI declarativo da JetBrains que estende o Jetpack Compose do Google para rodar em multiplas plataformas: Android, iOS, Desktop (JVM) e Web. No contexto desktop, a aplicacao roda sobre a JVM e renderiza usando Skia, a mesma engine grafica usada pelo Chrome e Flutter.
A proposta e clara: escrever UI uma vez em Kotlin e rodar em todas as plataformas. Embora cada plataforma tenha particularidades, a maior parte do codigo de interface e logica pode ser compartilhada.
Configuracao do projeto
A maneira mais rapida de iniciar e usar o Kotlin Multiplatform Wizard da JetBrains ou o template oficial. Vamos configurar manualmente para entender cada parte.
Estrutura do projeto
meu-app-desktop/
build.gradle.kts
settings.gradle.kts
gradle.properties
src/
main/
kotlin/
Main.kt
resources/
icone.png
build.gradle.kts
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("jvm") version "2.1.0"
id("org.jetbrains.compose") version "1.7.3"
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0"
}
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.material3)
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "MeuApp"
packageVersion = "1.0.0"
}
}
}
Ponto de entrada
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meu App Desktop"
) {
App()
}
}
@Composable
fun App() {
MaterialTheme {
var texto by remember { mutableStateOf("") }
var itens by remember { mutableStateOf(listOf<String>()) }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(
text = "Gerenciador de Tarefas",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = texto,
onValueChange = { texto = it },
label = { Text("Nova tarefa") },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
if (texto.isNotBlank()) {
itens = itens + texto
texto = ""
}
}) {
Text("Adicionar")
}
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(itens.size) { indice ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = itens[indice],
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
itens = itens.filterIndexed { i, _ -> i != indice }
}) {
Icon(Icons.Default.Delete, "Remover")
}
}
}
}
}
}
}
}
Para executar, use ./gradlew run no terminal.
Gerenciamento de janelas
Compose Multiplatform oferece controle completo sobre janelas, incluindo tamanho, posicao, icone e comportamento.
Multiplas janelas
fun main() = application {
var mostrarConfiguracoes by remember { mutableStateOf(false) }
Window(
onCloseRequest = ::exitApplication,
title = "Janela Principal",
state = rememberWindowState(
width = 800.dp,
height = 600.dp
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Janela Principal")
Button(onClick = { mostrarConfiguracoes = true }) {
Text("Abrir Configuracoes")
}
}
}
if (mostrarConfiguracoes) {
Window(
onCloseRequest = { mostrarConfiguracoes = false },
title = "Configuracoes",
state = rememberWindowState(
width = 400.dp,
height = 300.dp
)
) {
Text("Painel de Configuracoes", modifier = Modifier.padding(16.dp))
}
}
}
Dialogo personalizado
@Composable
fun DialogoConfirmacao(
titulo: String,
mensagem: String,
onConfirmar: () -> Unit,
onCancelar: () -> Unit
) {
Dialog(
onCloseRequest = onCancelar,
title = titulo,
state = rememberDialogState(width = 350.dp, height = 200.dp)
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(mensagem)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onCancelar) { Text("Cancelar") }
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = onConfirmar) { Text("Confirmar") }
}
}
}
}
Menus nativos
Aplicacoes desktop geralmente possuem barra de menus. Compose Multiplatform suporta menus nativos da plataforma:
Window(
onCloseRequest = ::exitApplication,
title = "Editor de Texto"
) {
MenuBar {
Menu("Arquivo") {
Item("Novo", shortcut = KeyShortcut(Key.N, meta = true)) {
println("Novo arquivo")
}
Item("Abrir", shortcut = KeyShortcut(Key.O, meta = true)) {
println("Abrir arquivo")
}
Item("Salvar", shortcut = KeyShortcut(Key.S, meta = true)) {
println("Salvar arquivo")
}
Separator()
Item("Sair") { exitApplication() }
}
Menu("Editar") {
Item("Desfazer", shortcut = KeyShortcut(Key.Z, meta = true)) {}
Item("Refazer", shortcut = KeyShortcut(Key.Z, meta = true, shift = true)) {}
}
}
// Conteudo da janela
TextField(
value = texto,
onValueChange = { texto = it },
modifier = Modifier.fillMaxSize().padding(16.dp)
)
}
System Tray
Voce pode adicionar um icone na bandeja do sistema para que o app continue rodando em segundo plano:
fun main() = application {
var visivel by remember { mutableStateOf(true) }
Tray(
icon = painterResource("icone.png"),
tooltip = "Meu App",
menu = {
Item("Mostrar") { visivel = true }
Item("Sair") { exitApplication() }
}
)
if (visivel) {
Window(
onCloseRequest = { visivel = false },
title = "Meu App"
) {
Text("Feche a janela - o app continua na bandeja")
}
}
}
Acesso a arquivos do sistema
Diferente de aplicacoes mobile, apps desktop frequentemente precisam ler e escrever arquivos. Compose Multiplatform oferece dialogos de arquivo nativos:
@Composable
fun SeletorDeArquivo() {
var caminhoArquivo by remember { mutableStateOf("Nenhum arquivo selecionado") }
var conteudo by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = {
val dialogo = FileDialog(ComposeWindow(), "Selecionar arquivo", FileDialog.LOAD)
dialogo.filenameFilter = FilenameFilter { _, nome -> nome.endsWith(".txt") }
dialogo.isVisible = true
val arquivo = dialogo.file
val diretorio = dialogo.directory
if (arquivo != null && diretorio != null) {
val path = File(diretorio, arquivo)
caminhoArquivo = path.absolutePath
conteudo = path.readText()
}
}) {
Text("Abrir Arquivo")
}
Spacer(modifier = Modifier.height(8.dp))
Text("Arquivo: $caminhoArquivo")
Spacer(modifier = Modifier.height(8.dp))
Text(conteudo)
}
}
Atalhos de teclado
Aplicacoes desktop dependem fortemente de atalhos de teclado para produtividade:
@Composable
fun EditorComAtalhos() {
var texto by remember { mutableStateOf("") }
var salvo by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
// Atalhos podem ser tratados via KeyEvent
}
Box(
modifier = Modifier
.fillMaxSize()
.onPreviewKeyEvent { evento ->
if (evento.isMetaPressed && evento.key == Key.S && evento.type == KeyEventType.KeyDown) {
salvo = true
true
} else {
false
}
}
) {
Column(modifier = Modifier.padding(16.dp)) {
if (salvo) {
Text("Arquivo salvo!", color = MaterialTheme.colorScheme.primary)
}
TextField(
value = texto,
onValueChange = {
texto = it
salvo = false
},
modifier = Modifier.fillMaxSize()
)
}
}
}
Distribuicao do aplicativo
Para distribuir seu app, o plugin Compose gera instaladores nativos para cada plataforma:
# macOS: gera .dmg
./gradlew packageDmg
# Windows: gera .msi
./gradlew packageMsi
# Linux: gera .deb
./gradlew packageDeb
Voce pode personalizar metadados do instalador no build.gradle.kts:
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "MeuApp"
packageVersion = "1.0.0"
description = "Meu aplicativo desktop em Kotlin"
vendor = "Minha Empresa"
macOS {
iconFile.set(project.file("icone.icns"))
}
windows {
iconFile.set(project.file("icone.ico"))
menuGroup = "Meus Apps"
}
linux {
iconFile.set(project.file("icone.png"))
}
}
}
}
Conclusao
Compose Multiplatform para Desktop abre um caminho empolgante para desenvolvedores Kotlin que querem criar aplicacoes desktop modernas. A combinacao de UI declarativa, acesso completo a JVM e APIs nativas de cada sistema operacional resulta em uma experiencia de desenvolvimento produtiva. Se voce ja conhece Jetpack Compose no Android, a transicao para desktop e quase imediata. O investimento em aprender Compose se multiplica: o mesmo conhecimento vale para Android, iOS, Desktop e Web.