Compartilhar lógica de negócio entre plataformas com Kotlin Multiplatform já era realidade. Mas compartilhar a interface gráfica sempre foi o maior desafio. O Compose Multiplatform (CMP) da JetBrains muda esse cenário ao permitir que você escreva UI em Kotlin uma vez e rode no Android, iOS, Desktop e Web. Em 2026, com a estabilização do suporte a iOS e melhorias em todas as frentes, CMP se tornou uma alternativa séria para projetos multiplataforma.
O que é Compose Multiplatform?
Compose Multiplatform é um framework de UI declarativo da JetBrains construído sobre o Jetpack Compose do Google. A ideia é simples: se o Jetpack Compose funciona perfeitamente para Android, por que não levar o mesmo modelo para outras plataformas?
CMP usa o mecanismo de renderização Skia (a mesma engine do Chrome e do Flutter) para desenhar a UI de forma consistente em todas as plataformas. Isso significa que seus Composables rodam nativamente em:
| Plataforma | Status em 2026 | Renderização |
|---|---|---|
| Android | Estável (produção) | Nativo via Jetpack Compose |
| iOS | Beta avançado | Skia + UIKit integration |
| Desktop (JVM) | Estável (produção) | Skia via JVM |
| Web (Wasm) | Alpha/Experimental | Skia via WebAssembly |
Se você já trabalhou com Compose para Desktop, CMP leva essa experiência ao próximo nível ao unificar todos os targets.
Configurando um Projeto CMP
A maneira mais rápida de começar é com o Kotlin Multiplatform Wizard da JetBrains (kmp.jetbrains.com). Mas vamos entender a estrutura manualmente.
Estrutura do Projeto
meu-app-cmp/
├── composeApp/
│ ├── src/
│ │ ├── commonMain/ # UI e lógica compartilhada
│ │ ├── androidMain/ # Código Android específico
│ │ ├── iosMain/ # Código iOS específico
│ │ └── desktopMain/ # Código Desktop específico
│ └── build.gradle.kts
├── iosApp/ # Projeto Xcode wrapper
├── build.gradle.kts
└── settings.gradle.kts
Configuração do Gradle
// composeApp/build.gradle.kts
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
}
kotlin {
androidTarget()
jvm("desktop")
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { target ->
target.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
}
androidMain.dependencies {
implementation(compose.preview)
implementation("androidx.activity:activity-compose:1.9.0")
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}
Compartilhando UI entre Plataformas
O coração do CMP é o source set commonMain. Todo código colocado aqui roda em todas as plataformas. Vamos criar uma tela de perfil de usuário compartilhada:
// commonMain/kotlin/ui/PerfilScreen.kt
@Composable
fun PerfilScreen(usuario: Usuario) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Avatar circular
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Text(
text = usuario.nome.first().toString(),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = usuario.nome,
style = MaterialTheme.typography.headlineMedium
)
Text(
text = usuario.email,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(32.dp))
// Lista de informações
InfoCard("Cargo", usuario.cargo)
InfoCard("Localização", usuario.cidade)
InfoCard("Membro desde", usuario.membroDesde)
}
}
@Composable
fun InfoCard(label: String, valor: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = valor,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
Esse código roda exatamente igual no Android, iOS e Desktop. Sem adaptações, sem #ifdef, sem código duplicado.
Código Específico por Plataforma com expect/actual
Nem tudo pode ser compartilhado. Para funcionalidades específicas de cada plataforma, o KMP oferece o mecanismo expect/actual:
// commonMain — declaração expect
expect fun compartilharTexto(texto: String)
expect fun obterVersaoApp(): String
// androidMain — implementação actual
actual fun compartilharTexto(texto: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, texto)
}
context.startActivity(Intent.createChooser(intent, "Compartilhar"))
}
actual fun obterVersaoApp(): String {
return BuildConfig.VERSION_NAME
}
// iosMain — implementação actual
actual fun compartilharTexto(texto: String) {
val activityController = UIActivityViewController(
activityItems = listOf(texto),
applicationActivities = null
)
UIApplication.sharedApplication.keyWindow?.rootViewController
?.presentViewController(activityController, true, null)
}
actual fun obterVersaoApp(): String {
return NSBundle.mainBundle.infoDictionary
?.get("CFBundleShortVersionString") as? String ?: "1.0"
}
O compilador garante que toda declaração expect tenha sua contrapartida actual em cada target. Sem actual, o build falha — segurança em tempo de compilação.
Navegação em Compose Multiplatform
Navegação é um dos pontos que mais evoluiu em 2026. A biblioteca oficial Compose Navigation agora suporta KMP nativamente:
// commonMain
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onPerfilClick = { navController.navigate("perfil/$it") },
onConfigClick = { navController.navigate("configuracoes") }
)
}
composable("perfil/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId") ?: return@composable
PerfilScreen(
userId = userId,
onVoltar = { navController.popBackStack() }
)
}
composable("configuracoes") {
ConfiguracoesScreen(
onVoltar = { navController.popBackStack() }
)
}
}
}
Alternativas populares para navegação multiplataforma incluem:
- Voyager — API simples e intuitiva, inspirada no Jetpack Navigation
- Decompose — arquitetura baseada em componentes, boa para apps complexos
- Appyx — navegação com animações avançadas
Gerenciamento de Estado
Para gerenciamento de estado, CMP funciona com as mesmas ferramentas do Compose:
// ViewModel compartilhado usando kotlinx-coroutines e Flow
class PerfilViewModel(
private val repository: UsuarioRepository
) {
private val _uiState = MutableStateFlow<PerfilUiState>(PerfilUiState.Carregando)
val uiState: StateFlow<PerfilUiState> = _uiState.asStateFlow()
fun carregarPerfil(userId: String) {
CoroutineScope(Dispatchers.Default).launch {
_uiState.value = PerfilUiState.Carregando
try {
val usuario = repository.buscarUsuario(userId)
_uiState.value = PerfilUiState.Sucesso(usuario)
} catch (e: Exception) {
_uiState.value = PerfilUiState.Erro(e.message ?: "Erro desconhecido")
}
}
}
}
sealed class PerfilUiState {
data object Carregando : PerfilUiState()
data class Sucesso(val usuario: Usuario) : PerfilUiState()
data class Erro(val mensagem: String) : PerfilUiState()
}
Se você já trabalha com Coroutines e Flow no Android, a transição para CMP é natural. Confira nosso guia completo de Coroutines para dominar esses conceitos.
Limitações e Considerações
CMP amadureceu muito, mas ainda tem limitações que você precisa considerar:
iOS ainda em Beta
O suporte a iOS avançou significativamente, mas ainda é beta. Widgets de sistema, deep links avançados e algumas APIs do UIKit exigem código nativo. Para apps em produção no iOS, teste extensivamente.
Performance no iOS
A renderização via Skia no iOS tem overhead comparado a SwiftUI nativo. Para a maioria dos apps, a diferença é imperceptível. Para apps com animações pesadas ou listas muito longas, monitore o desempenho.
Tamanho do binário
Apps CMP tendem a ter binários maiores do que apps nativos puros, pois incluem o runtime do Skia. No Android, o impacto é menor (~2-3 MB extra). No iOS, pode chegar a ~10-15 MB adicionais.
Ecossistema de bibliotecas
Nem todas as bibliotecas do Jetpack Compose estão disponíveis para KMP. Antes de adotar CMP, verifique se suas dependências-chave suportam multiplataforma.
CMP vs Flutter vs React Native
Uma comparação inevitável. Cada framework tem seus pontos fortes:
| Aspecto | Compose Multiplatform | Flutter | React Native |
|---|---|---|---|
| Linguagem | Kotlin | Dart | JavaScript/TypeScript |
| Renderização | Skia | Skia (Impeller) | Componentes nativos |
| Código compartilhado | UI + Lógica | UI + Lógica | UI + Lógica |
| Integração nativa | Excelente (KMP) | Boa (Platform Channels) | Boa (Native Modules) |
| Curva de aprendizado | Baixa (se sabe Kotlin) | Média | Baixa (se sabe JS) |
| Maturidade | Em crescimento | Madura | Madura |
Se sua equipe já domina Kotlin e tem um app Android com Jetpack Compose, CMP é a escolha mais natural — o código existente pode ser reaproveitado. Confira nossa comparação detalhada entre KMP e Flutter e entre KMP e React Native.
Para quem vem de outras linguagens, vale conhecer como cada ecossistema resolve o desafio de multiplataforma. Go também tem propostas de UI multiplataforma como Gio, embora com foco diferente. Já Python investe em frameworks como Kivy e BeeWare para o mesmo objetivo.
Exemplo Completo: App de Tarefas
Para consolidar, veja uma tela funcional de lista de tarefas compartilhada:
@Composable
fun TarefasScreen(viewModel: TarefasViewModel) {
val tarefas by viewModel.tarefas.collectAsState()
var novaTarefa by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(title = { Text("Minhas Tarefas") })
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
// Campo de nova tarefa
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = novaTarefa,
onValueChange = { novaTarefa = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Nova tarefa...") },
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (novaTarefa.isNotBlank()) {
viewModel.adicionar(novaTarefa)
novaTarefa = ""
}
}
) {
Icon(Icons.Default.Add, "Adicionar")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Lista de tarefas
LazyColumn {
items(tarefas) { tarefa ->
TarefaItem(
tarefa = tarefa,
onToggle = { viewModel.alternarConclusao(tarefa.id) },
onRemover = { viewModel.remover(tarefa.id) }
)
}
}
}
}
}
Conclusão
Compose Multiplatform em 2026 é uma tecnologia madura o suficiente para projetos reais — especialmente se sua equipe já trabalha com Kotlin e Jetpack Compose. O compartilhamento de UI entre Android, iOS e Desktop reduz significativamente o esforço de desenvolvimento e manutenção.
Para começar, recomendo explorar nosso guia de Kotlin Multiplatform Mobile e o artigo sobre o futuro do KMP. Se você quer entender a base do Compose, confira o guia de Jetpack Compose.
O ecossistema Kotlin continua evoluindo — e o CMP é uma das peças mais empolgantes desse quebra-cabeça.
Se você busca performance nativa sem garbage collector para componentes críticos do seu app multiplataforma, vale explorar como Rust aborda interoperabilidade com mobile e WebAssembly.