Escrever o mesmo código duas (ou três) vezes para plataformas diferentes é uma dor que todo time de desenvolvimento conhece. Kotlin Multiplatform (KMP) resolve isso permitindo compartilhar lógica de negócio entre Android, iOS, web e backend — tudo em Kotlin. Vamos entender como funciona.
O que é Kotlin Multiplatform?
Kotlin Multiplatform é uma tecnologia da JetBrains que permite escrever código uma vez e usar em múltiplas plataformas. Diferente de soluções como Flutter ou React Native, o KMP não substitui a UI nativa — ele compartilha a lógica de negócio enquanto cada plataforma mantém sua interface nativa.
Isso significa:
- Android: UI com Jetpack Compose, lógica compartilhada em Kotlin
- iOS: UI com SwiftUI, lógica compartilhada em Kotlin (compilado para framework nativo)
- Web: UI com frameworks JS/Kotlin, lógica compartilhada
- Backend: mesmos modelos e regras de negócio do app
Por que KMP e não Flutter/React Native?
Cada abordagem tem seus méritos, mas o KMP se destaca por:
- UI nativa real: a interface de cada plataforma usa os componentes nativos
- Adoção gradual: você pode integrar KMP em projetos existentes
- Performance nativa: sem bridge, sem overhead de runtime
- Flexibilidade: compartilhe só o que faz sentido — de 20% a 80% do código
- Kotlin: uma das linguagens mais amadas pelos desenvolvedores
Estrutura de um projeto KMP
Um projeto KMP típico é organizado assim:
meu-app/
├── shared/ # Módulo compartilhado
│ └── src/
│ ├── commonMain/ # Código comum a todas as plataformas
│ ├── commonTest/ # Testes compartilhados
│ ├── androidMain/ # Código específico Android
│ ├── iosMain/ # Código específico iOS
│ └── jvmMain/ # Código específico JVM (backend)
├── androidApp/ # Aplicativo Android
├── iosApp/ # Aplicativo iOS (projeto Xcode)
└── build.gradle.kts
Configurando o projeto
O build.gradle.kts do módulo compartilhado:
plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization")
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
implementation("io.ktor:ktor-client-core:3.0.0")
implementation("io.ktor:ktor-client-content-negotiation:3.0.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-android:3.0.0")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.0.0")
}
jvmMain.dependencies {
implementation("io.ktor:ktor-client-cio:3.0.0")
}
}
}
Código compartilhado: modelos e regras de negócio
Tudo que vai em commonMain é compartilhado entre todas as plataformas:
// commonMain/kotlin/model/Produto.kt
@Serializable
data class Produto(
val id: Long,
val nome: String,
val descricao: String,
val preco: Double,
val imagemUrl: String
)
// commonMain/kotlin/model/Carrinho.kt
data class Carrinho(
val itens: List<ItemCarrinho> = emptyList()
) {
val total: Double
get() = itens.sumOf { it.produto.preco * it.quantidade }
val quantidadeTotal: Int
get() = itens.sumOf { it.quantidade }
fun adicionar(produto: Produto, quantidade: Int = 1): Carrinho {
val existente = itens.find { it.produto.id == produto.id }
return if (existente != null) {
copy(itens = itens.map {
if (it.produto.id == produto.id) it.copy(quantidade = it.quantidade + quantidade)
else it
})
} else {
copy(itens = itens + ItemCarrinho(produto, quantidade))
}
}
fun remover(produtoId: Long): Carrinho {
return copy(itens = itens.filter { it.produto.id != produtoId })
}
}
data class ItemCarrinho(val produto: Produto, val quantidade: Int)
Esse código roda igualzinho no Android, no iOS e no backend. Escreva uma vez, teste uma vez, use em todo lugar.
Chamadas de rede compartilhadas
Usando Ktor Client, as chamadas de API ficam no código comum:
// commonMain/kotlin/api/ProdutoApi.kt
class ProdutoApi(private val client: HttpClient) {
suspend fun buscarProdutos(): List<Produto> {
return client.get("https://api.meuapp.com.br/produtos")
.body<List<Produto>>()
}
suspend fun buscarProduto(id: Long): Produto {
return client.get("https://api.meuapp.com.br/produtos/$id")
.body<Produto>()
}
}
// commonMain/kotlin/api/HttpClientFactory.kt
fun criarHttpClient() = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
}
expect/actual: código específico por plataforma
Quando você precisa de algo específico de cada plataforma, usa o mecanismo expect/actual:
// commonMain — declaração expect (como uma interface)
expect fun plataformaAtual(): String
expect class ArmazenamentoLocal {
fun salvar(chave: String, valor: String)
fun ler(chave: String): String?
}
// androidMain — implementação actual
actual fun plataformaAtual(): String = "Android ${Build.VERSION.SDK_INT}"
actual class ArmazenamentoLocal(context: Context) {
private val prefs = context.getSharedPreferences("app", Context.MODE_PRIVATE)
actual fun salvar(chave: String, valor: String) {
prefs.edit().putString(chave, valor).apply()
}
actual fun ler(chave: String): String? = prefs.getString(chave, null)
}
// iosMain — implementação actual
actual fun plataformaAtual(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
actual class ArmazenamentoLocal {
private val defaults = NSUserDefaults.standardUserDefaults
actual fun salvar(chave: String, valor: String) {
defaults.setObject(valor, forKey = chave)
}
actual fun ler(chave: String): String? = defaults.stringForKey(chave)
}
ViewModel compartilhado
Com KMP, até a camada de ViewModel pode ser compartilhada:
// commonMain/kotlin/viewmodel/ProdutoListViewModel.kt
class ProdutoListViewModel(private val api: ProdutoApi) {
private val _state = MutableStateFlow<ProdutoListState>(ProdutoListState.Carregando)
val state: StateFlow<ProdutoListState> = _state.asStateFlow()
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun carregar() {
scope.launch {
_state.value = ProdutoListState.Carregando
try {
val produtos = api.buscarProdutos()
_state.value = ProdutoListState.Sucesso(produtos)
} catch (e: Exception) {
_state.value = ProdutoListState.Erro(e.message ?: "Erro desconhecido")
}
}
}
fun limpar() { scope.cancel() }
}
sealed class ProdutoListState {
data object Carregando : ProdutoListState()
data class Sucesso(val produtos: List<Produto>) : ProdutoListState()
data class Erro(val mensagem: String) : ProdutoListState()
}
Usando no iOS (SwiftUI)
O código Kotlin compartilhado é acessado no Swift de forma transparente:
// Exemplo de como fica no SwiftUI (pseudo-código para contexto):
// struct ProdutoListView: View {
// @StateObject var viewModel = ProdutoListViewModel(api: ProdutoApi(...))
//
// var body: some View {
// switch viewModel.state {
// case .carregando: ProgressView()
// case .sucesso(let produtos): List(produtos) { ... }
// case .erro(let msg): Text(msg)
// }
// }
// }
Compose Multiplatform
Para quem quer compartilhar até a UI, existe o Compose Multiplatform — Jetpack Compose que roda em Android, iOS, Desktop e Web:
@Composable
fun ProdutoCard(produto: Produto, onClicar: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClicar() }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(produto.nome, style = MaterialTheme.typography.titleMedium)
Text(produto.descricao, style = MaterialTheme.typography.bodyMedium)
Text(
"R$ ${"%.2f".format(produto.preco)}",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
}
}
}
Esse composable roda em todas as plataformas!
Conclusão
Kotlin Multiplatform é a aposta mais promissora para compartilhamento de código entre plataformas em 2026. A abordagem incremental, o respeito pela UI nativa e a maturidade crescente do ecossistema fazem do KMP uma escolha cada vez mais segura para times de todos os tamanhos.
Comece pelo módulo de modelos e regras de negócio, vá expandindo aos poucos, e veja a produtividade do seu time decolar. O futuro é multiplataforma — e é em Kotlin!