Kotlin Multiplatform (KMP) permite compartilhar lógica de negócio entre Android, iOS, desktop e web usando uma única base de código Kotlin. Neste tutorial, vamos configurar um projeto KMP do zero, entender o mecanismo expect/actual, criar um módulo compartilhado, integrar com Compose Multiplatform para UI, usar Ktor Client para requisições HTTP e configurar injeção de dependências com Koin.
O que é Kotlin Multiplatform?
Diferente de soluções como Flutter ou React Native que substituem completamente a UI nativa, o KMP foca em compartilhar a lógica de negócio — modelos de dados, networking, validações, regras de negócio — enquanto permite que cada plataforma use sua própria tecnologia de interface. Com a chegada do Compose Multiplatform, agora também é possível compartilhar a camada de UI entre Android, iOS e desktop.
O KMP é uma tecnologia estável da JetBrains, já usada em produção por empresas como Netflix, Philips, VMWare e Cash App. A ideia central é simples: escreva código Kotlin no módulo commonMain e ele compila para JVM (Android), nativo (iOS via Kotlin/Native) e JavaScript (web).
Passo 1: Configuração do Projeto
A forma mais prática de criar um projeto KMP é usando o wizard em kmp.jetbrains.com. Para entender a fundo, vamos ver a configuração manual. A estrutura de diretórios fica assim:
meu-projeto-kmp/
├── shared/
│ └── src/
│ ├── commonMain/kotlin/ # Código compartilhado
│ ├── commonTest/kotlin/ # Testes compartilhados
│ ├── androidMain/kotlin/ # Código Android-específico
│ └── iosMain/kotlin/ # Código iOS-específico
├── androidApp/ # Aplicação Android
├── iosApp/ # Aplicação iOS (Xcode)
└── build.gradle.kts
O build.gradle.kts do módulo compartilhado:
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization")
}
kotlin {
androidTarget {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
implementation("io.insert-koin:koin-core:3.5.6")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.12")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.12")
}
}
}
android {
namespace = "com.exemplo.shared"
compileSdk = 34
defaultConfig {
minSdk = 24
}
}
Note como as dependências são organizadas por source set: commonMain para código compartilhado, androidMain e iosMain para implementações específicas de plataforma. O Ktor Client, por exemplo, usa OkHttp no Android e Darwin no iOS.
Passo 2: Mecanismo expect/actual
O expect/actual é o recurso que permite declarar uma API no código comum e fornecer implementações específicas por plataforma. Pense como uma interface resolvida em tempo de compilação.
// commonMain/kotlin/Platform.kt
expect class Platform() {
val nome: String
val versao: String
}
expect fun obterTimestampAtual(): Long
// androidMain/kotlin/Platform.android.kt
actual class Platform actual constructor() {
actual val nome: String = "Android ${android.os.Build.VERSION.SDK_INT}"
actual val versao: String = android.os.Build.VERSION.RELEASE
}
actual fun obterTimestampAtual(): Long = System.currentTimeMillis()
// iosMain/kotlin/Platform.ios.kt
import platform.UIKit.UIDevice
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual class Platform actual constructor() {
actual val nome: String = UIDevice.currentDevice.systemName()
actual val versao: String = UIDevice.currentDevice.systemVersion
}
actual fun obterTimestampAtual(): Long =
(NSDate().timeIntervalSince1970 * 1000).toLong()
No código comum, use Platform() normalmente — o compilador escolhe a implementação correta para cada target. O ideal é minimizar o uso de expect/actual e mover a maior parte da lógica para commonMain, usando-o apenas quando realmente precisa de APIs nativas.
Passo 3: Módulo Compartilhado com Lógica de Negócio
Vamos criar um módulo compartilhado que busca dados de uma API e aplica regras de negócio:
// commonMain/kotlin/modelo/Produto.kt
import kotlinx.serialization.Serializable
@Serializable
data class Produto(
val id: Int,
val nome: String,
val preco: Double,
val categoria: String
)
@Serializable
data class RespostaApi<T>(
val dados: List<T>,
val total: Int
)
// commonMain/kotlin/repositorio/ProdutoRepositorio.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class ProdutoRepositorio(private val client: HttpClient) {
suspend fun buscarProdutos(): List<Produto> {
val resposta: RespostaApi<Produto> = client.get("https://api.exemplo.com/produtos").body()
return resposta.dados
}
suspend fun buscarPorCategoria(categoria: String): List<Produto> {
return buscarProdutos().filter { it.categoria == categoria }
}
}
// commonMain/kotlin/usecase/ListarProdutosUseCase.kt
class ListarProdutosUseCase(private val repositorio: ProdutoRepositorio) {
suspend fun executar(filtroPrecoMaximo: Double? = null): List<Produto> {
val produtos = repositorio.buscarProdutos()
return if (filtroPrecoMaximo != null) {
produtos.filter { it.preco <= filtroPrecoMaximo }
.sortedBy { it.preco }
} else {
produtos.sortedBy { it.nome }
}
}
}
Toda essa lógica compila para Android, iOS e qualquer outro target. As data classes com @Serializable funcionam em todas as plataformas graças ao kotlinx.serialization, que gera código de serialização em tempo de compilação.
Passo 4: Compose Multiplatform para UI Compartilhada
Com o Compose Multiplatform, você pode compartilhar também a camada de UI entre Android, iOS e desktop usando a mesma API do Jetpack Compose:
// commonMain/kotlin/ui/TelaProdutos.kt
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun TelaProdutos(viewModel: ProdutosViewModel) {
val estado by viewModel.estado.collectAsState()
LaunchedEffect(Unit) {
viewModel.carregarProdutos()
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(
text = "Produtos",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
when {
estado.carregando -> CircularProgressIndicator()
estado.erro != null -> Text("Erro: ${estado.erro}", color = MaterialTheme.colorScheme.error)
else -> LazyColumn {
items(estado.produtos) { produto ->
CartaoProduto(produto)
}
}
}
}
}
@Composable
fun CartaoProduto(produto: Produto) {
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = produto.nome, style = MaterialTheme.typography.titleMedium)
Text(text = "R$ ${"%.2f".format(produto.preco)}")
Text(text = produto.categoria, style = MaterialTheme.typography.bodySmall)
}
}
}
O Compose Multiplatform usa exatamente a mesma API do Jetpack Compose do Android. O compilador gera renderização nativa para cada plataforma — Skia para iOS e desktop, Android Canvas para Android.
Passo 5: Ktor Client Multiplatform
A configuração do Ktor Client no código comum com engines específicas por plataforma:
// commonMain/kotlin/rede/HttpClientFactory.kt
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
expect fun criarHttpClient(): HttpClient
fun criarHttpClientComum(engine: HttpClientEngine): HttpClient {
return HttpClient(engine) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
}
// androidMain/kotlin/rede/HttpClientFactory.android.kt
import io.ktor.client.engine.okhttp.*
actual fun criarHttpClient(): HttpClient = criarHttpClientComum(OkHttp.create())
// iosMain/kotlin/rede/HttpClientFactory.ios.kt
import io.ktor.client.engine.darwin.*
actual fun criarHttpClient(): HttpClient = criarHttpClientComum(Darwin.create())
Passo 6: Injeção de Dependências com Koin
O Koin é um framework de DI leve que funciona nativamente com KMP, sem geração de código nem reflection pesada:
// commonMain/kotlin/di/Modulos.kt
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val moduloCompartilhado = module {
single { criarHttpClient() }
singleOf(::ProdutoRepositorio)
singleOf(::ListarProdutosUseCase)
factory { ProdutosViewModel(get()) }
}
// androidMain: inicialização no Application
class MeuApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MeuApp)
modules(moduloCompartilhado)
}
}
}
// iosMain: inicialização para Swift
fun inicializarKoin() {
startKoin {
modules(moduloCompartilhado)
}
}
No lado iOS (Swift), chame IosMainKt.inicializarKoin() no AppDelegate. O Koin gerencia o grafo de dependências de forma idêntica em ambas as plataformas.
Erros Comuns
1. Usar APIs específicas de plataforma no commonMain: O código em commonMain só pode usar a biblioteca padrão do Kotlin e dependências multiplatform. Se precisar de java.io.File, use expect/actual ou a biblioteca okio que é multiplatform.
2. Esquecer de adicionar todos os targets iOS: Sempre inclua iosX64() (simulador Intel), iosArm64() (dispositivo real) e iosSimulatorArm64() (simulador Apple Silicon). Omitir qualquer um causa erros confusos ao compilar.
3. Serialização não funciona: O kotlinx.serialization exige o plugin de compilador kotlin("plugin.serialization"). Sem ele, a annotation @Serializable não gera o serializer e você recebe erros em runtime.
4. Coroutines no iOS congelando: No Kotlin/Native, o modelo de memória antigo tinha restrições com coroutines. Use Kotlin 1.9+ que traz o novo modelo de memória por padrão, eliminando esse problema.
5. Dependências com versões incompatíveis: No KMP, todas as dependências devem suportar seus targets. Verifique no repositório da biblioteca se ela publica artefatos para iosArm64, iosX64, etc. Use o Kotlin Multiplatform Compatibility Guide como referência.
Conclusão e Próximos Passos
Neste tutorial, construímos um projeto Kotlin Multiplatform completo com módulo compartilhado, mecanismo expect/actual, UI com Compose Multiplatform, networking com Ktor Client e DI com Koin. O KMP representa o futuro do desenvolvimento multiplataforma, permitindo reaproveitar lógica crítica sem sacrificar a experiência nativa de cada plataforma.
Como próximos passos, explore SQLDelight para persistência local multiplatform, Napier para logging multiplataforma, e SKIE para melhorar a interoperabilidade Swift-Kotlin. Para aprofundar seus conhecimentos em coroutines compartilhadas, confira nosso tutorial sobre Coroutines Avançadas e o guia Kotlin para Backend onde exploramos o Ktor em mais detalhes.