Kotlin Multiplatform: compartilhando lógica entre Android, iOS e web
1. Introdução ao Kotlin Multiplatform (KMP)
Kotlin Multiplatform (KMP) é uma tecnologia da JetBrains que permite compartilhar código-fonte entre múltiplas plataformas — Android, iOS, web e desktop — mantendo a liberdade de construir interfaces nativas em cada uma delas. Diferentemente de soluções como Flutter (que impõe um framework de UI próprio) ou React Native (que abstrai componentes nativos via JavaScript), o KMP adota uma abordagem modular: você escreve a lógica de negócio uma única vez no módulo commonMain e implementa apenas as partes específicas de cada plataforma nos módulos androidMain, iosMain e wasmJsMain.
A arquitetura básica do KMP é centrada em três camadas:
- Módulo compartilhado (shared): contém toda a lógica de negócio, modelos de dados, chamadas de API e regras de validação.
- Módulos específicos de plataforma: implementam funcionalidades nativas como armazenamento local, sensores ou acesso a hardware.
- Aplicativos finais: consomem o módulo compartilhado e constroem a UI nativa (Android com Jetpack Compose, iOS com SwiftUI, web com Compose for Web ou React).
As vantagens estratégicas são claras: redução de custos de desenvolvimento, consistência de regras de negócio entre plataformas e integração total com ecossistemas existentes (você pode adicionar KMP a um projeto Android ou iOS já em produção).
2. Configuração do Projeto KMP com Compose Multiplatform
Para iniciar um projeto KMP, utilize o IntelliJ IDEA ou Android Studio com o plugin Kotlin Multiplatform instalado. A estrutura típica contém dois módulos principais:
shared: módulo Kotlin puro com targets Android, iOS e Web.composeApp: módulo que utiliza Compose Multiplatform para construir a UI compartilhada.
Dependências essenciais no arquivo build.gradle.kts do módulo shared:
plugins {
kotlin("multiplatform") version "2.0.21"
id("com.android.library")
kotlin("plugin.serialization") version "2.0.21"
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
wasmJs {
browser()
}
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:3.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("app.cash.sqldelight:runtime:2.0.2")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:3.0.1")
implementation("app.cash.sqldelight:android-driver:2.0.2")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.0.1")
implementation("app.cash.sqldelight:native-driver:2.0.2")
}
wasmJsMain.dependencies {
implementation("io.ktor:ktor-client-js:3.0.1")
implementation("app.cash.sqldelight:wasm-driver:2.0.2")
}
}
}
A configuração de targets define quais plataformas serão suportadas. Para iOS, são necessários três targets (x64, arm64 e simulador). Para web, utiliza-se wasmJs (WebAssembly) ou js (JavaScript tradicional).
3. Compartilhando Lógica de Negócio com expect/actual
O mecanismo expect/actual permite declarar funções ou classes no módulo comum que exigem implementações específicas por plataforma. Exemplo prático: acesso a preferências do usuário.
No módulo commonMain:
// commonMain
expect class Settings {
fun getString(key: String): String?
fun putString(key: String, value: String)
}
Implementação para Android (androidMain):
// androidMain
actual class Settings(private val context: android.content.Context) {
private val prefs = context.getSharedPreferences("app_prefs", android.content.Context.MODE_PRIVATE)
actual fun getString(key: String): String? = prefs.getString(key, null)
actual fun putString(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
}
Implementação para iOS (iosMain):
// iosMain
actual class Settings {
private val defaults = platform.Foundation.NSUserDefaults.standardUserDefaults
actual fun getString(key: String): String? = defaults.stringForKey(key)
actual fun putString(key: String, value: String) {
defaults.setObject(value, forKey = key)
}
}
Implementação para Web (wasmJsMain):
// wasmJsMain
actual class Settings {
actual fun getString(key: String): String? =
kotlinx.browser.window.localStorage.getItem(key)
actual fun putString(key: String, value: String) {
kotlinx.browser.window.localStorage.setItem(key, value)
}
}
4. Consumo de APIs com Ktor Client
Ktor Client é a biblioteca HTTP oficial da JetBrains para KMP. Configuração no módulo comum:
// commonMain
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class User(val id: Int, val name: String, val email: String)
class ApiClient {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
install(HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 10_000
}
}
suspend fun getUsers(): List<User> {
val response: HttpResponse = client.get("https://jsonplaceholder.typicode.com/users")
return response.body()
}
}
Cada plataforma utiliza sua engine específica: OkHttp para Android, Darwin para iOS e JS para Web. O Ktor gerencia automaticamente a engine correta com base no target.
5. Persistência de Dados com SQLDelight
SQLDelight gera código Kotlin a partir de arquivos .sq compartilhados. Crie o arquivo Task.sq no módulo commonMain:
-- commonMain/sqldelight/app/cash/sqldelight/tasks/Task.sq
CREATE TABLE Task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
);
getAll:
SELECT * FROM Task ORDER BY id DESC;
insert:
INSERT INTO Task(title, completed) VALUES (?, ?);
updateCompleted:
UPDATE Task SET completed = ? WHERE id = ?;
Repositório compartilhado:
// commonMain
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import kotlinx.coroutines.flow.Flow
class TaskRepository(private val driver: SqlDriver) {
private val database = Database(driver)
private val queries = database.taskQueries
fun getAllTasks(): Flow<List<Task>> = queries.getAll().asFlow().mapToList()
fun addTask(title: String) {
queries.insert(title, 0)
}
fun toggleTask(id: Long, completed: Boolean) {
queries.updateCompleted(if (completed) 1 else 0, id)
}
}
As implementações do driver específico são fornecidas via expect/actual ou injeção de dependência.
6. Navegação e Estado com Compose Multiplatform
Compose Multiplatform permite compartilhar a UI entre Android, iOS e Web. Exemplo de navegação simples:
// composeApp/src/commonMain
import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun App() {
MaterialTheme {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(
onNavigateToDetail = { taskId ->
navController.navigate("detail/$taskId")
}
)
}
composable("detail/{taskId}") { backStackEntry ->
val taskId = backStackEntry.arguments?.getString("taskId") ?: "0"
DetailScreen(taskId.toLong())
}
}
}
}
O gerenciamento de estado utiliza ViewModel com StateFlow, compatível com KMP via bibliotecas como androidx.lifecycle:lifecycle-viewmodel-compose (para Android) e adaptações para iOS/Web.
7. Testes e CI/CD para Projetos KMP
Testes unitários no módulo comum:
// commonTest
import kotlin.test.Test
import kotlin.test.assertEquals
class TaskRepositoryTest {
@Test
fun testAddTask() {
// Teste com driver in-memory
val driver = app.cash.sqldelight.drivers.native.NativeSqliteDriver(Database.Schema, "test.db")
val repo = TaskRepository(driver)
repo.addTask("Test task")
assertEquals(1, repo.getAllTasks().first().size)
}
}
Pipeline de CI com GitHub Actions:
name: KMP CI
on: [push]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build Android
run: ./gradlew :shared:assembleDebug
- name: Build iOS
run: ./gradlew :shared:iosX64MainBinaries
- name: Build Web
run: ./gradlew :composeApp:wasmJsBrowserDistribution
- name: Run Tests
run: ./gradlew :shared:allTests
8. Casos de Uso Reais e Limitações em 2025
Empresas como Netflix utilizam KMP para compartilhar lógica de streaming entre Android e iOS. Cash App (Block) usa KMP para seus modelos financeiros compartilhados. Em 2025, o suporte a bibliotecas nativas melhorou significativamente, mas ainda existem limitações:
- Complexidade de configuração iOS: requer Xcode e conhecimento de Swift/Objective-C para bridges nativas.
- Bibliotecas de terceiros: nem todas as bibliotecas Kotlin suportam KMP (principalmente as que dependem de reflexão ou Java puro).
- Performance em Web: o target Wasm ainda está amadurecendo, com overhead em comparação a JavaScript puro.
O futuro do KMP inclui Kotlin 2.1 com melhorias no compilador Wasm, integração com WASI para aplicações server-side e suporte expandido a desktop via JVM e LLVM.
Referências
- Documentação Oficial do Kotlin Multiplatform — Guia completo de configuração, targets e boas práticas oficiais da JetBrains.
- Ktor Client Documentation — Documentação oficial do Ktor Client para requisições HTTP multiplataforma.
- SQLDelight Documentation — Guia de uso do SQLDelight para persistência de dados em projetos KMP.
- Compose Multiplatform Tutorial — Tutorial oficial da JetBrains para construção de UIs compartilhadas com Compose Multiplatform.
- Kotlin Multiplatform by Example (GitHub) — Repositório oficial com exemplos práticos de projetos KMP para Android, iOS e Web.
- Netflix Technology Blog - Kotlin Multiplatform — Artigo técnico sobre como a Netflix utiliza KMP em produção.
- Cash App Engineering - SQLDelight and KMP — Experiência da Cash App com SQLDelight em projetos KMP.