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.