Kotlin: Introdução ao Kotlin Multiplatform

Última atualização: 2026-08-26

KMP é a visão definitiva do Kotlin — o OrderValidator do Charlie é escrito uma vez e reutilizado no backend JVM, app iOS, frontend JS e navegador Wasm. O mecanismo expect/actual mantém as diferenças de plataforma apenas nas bordas.

1. O que Você Aprenderá


2. A História Real de um Arquiteto

(1) Problema: Triplicando a Lógica de Negócio em Três Plataformas

A empresa do Charlie tem um backend JVM, um app iOS e um frontend JS. A mesma lógica de OrderValidator foi implementada três vezes em Java, Swift e TypeScript. Corrigir um bug exigia mudanças em três lugares — isso causou dois incidentes de comportamento inconsistente em um mês.

(2) Solução de Código Compartilhado do KMP

KOTLIN
// commonMain: UMA implementação para todas as plataformas
class OrderValidator {
    fun validate(order: Order): List<String> {
        val errors = mutableListOf<String>()
        if (!order.id.startsWith("ORD-")) errors.add("Invalid ID")
        if (order.total < 0) errors.add("Negative total")
        return errors
    }
}

Uma base de código, três plataformas. Corrija um bug uma vez, e o comportamento será naturalmente consistente em todos os lugares.


3. Arquitetura KMP

(1) Estrutura do Projeto

TEXT 📖 Somente leitura
shared/
├── src/
│   ├── commonMain/kotlin/       # Código compartilhado (todas as plataformas)
│   │   └── com/order/
│   │       ├── Order.kt
│   │       ├── OrderValidator.kt
│   │       └── Platform.kt      # declarações expect
│   ├── commonTest/kotlin/       # Testes compartilhados
│   ├── jvmMain/kotlin/          # Código específico da JVM
│   │   └── com/order/
│   │       └── Platform.kt      # actual para JVM
│   ├── iosMain/kotlin/          # Código específico do iOS
│   │   └── com/order/
│   │       └── Platform.kt      # actual para iOS
│   └── jsMain/kotlin/           # Código específico do JS
│       └── com/order/
│           └── Platform.kt      # actual para JS
└── build.gradle.kts

(2) Mecanismo expect / actual

KOTLIN
// commonMain: declare o que você precisa (expect)
expect fun getPlatformName(): String
expect class DateFormatter() {
    fun format(timestamp: Long): String
}

// jvmMain: forneça implementação JVM (actual)
actual fun getPlatformName(): String = "JVM"
actual class DateFormatter actual constructor() {
    actual fun format(timestamp: Long): String =
        java.text.SimpleDateFormat("yyyy-MM-dd").format(timestamp)
}

// iosMain: forneça implementação iOS (actual)
actual fun getPlatformName(): String = "iOS"
actual class DateFormatter actual constructor() {
    actual fun format(timestamp: Long): String {
        // Usa NSDateFormatter
        return NSDateFormatter().apply {
            dateFormat = "yyyy-MM-dd"
        }.stringFromDate(NSDate(timestamp / 1000.0))
    }
}

// jsMain: forneça implementação JS (actual)
actual fun getPlatformName(): String = "JS"
actual class DateFormatter actual constructor() {
    actual fun format(timestamp: Long): String {
        // Usa JavaScript Date
        return js("new Date(timestamp).toISOString().split('T')[0]")
    }
}

(3) Arquitetura Multiplataforma KMP

100%
flowchart TD
    A[commonMain<br/>Lógica de Negócio Compartilhada] --> B[jvmMain<br/>Actual JVM]
    A --> C[iosMain<br/>Actual iOS]
    A --> D[jsMain<br/>Actual JS]
    A --> E[wasmMain<br/>Actual Wasm]
    B --> B1[Spring Boot<br/>Backend]
    C --> C1[App iOS<br/>Interop Swift]
    D --> D1[Node.js / Navegador]
    E --> E1[Runtime Web Assembly]

4. Configuração Gradle Multiplataforma

(1) build.gradle.kts

KOTLIN
plugins {
    kotlin("multiplatform") version "1.9.22"
}

group = "com.order"
version = "1.0.0"

repositories {
    mavenCentral()
}

kotlin {
    // Declarar plataformas alvo
    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "17"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    js(IR) {
        browser()
        nodejs()
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val jvmMain by getting
        val jvmTest by getting
        val iosMain by getting
        val jsMain by getting
    }
}

5. Estratégia de Módulo Compartilhado

(1) O que Compartilhar e o que Não Compartilhar

Camada Estratégia de Compartilhamento Motivo
Modelos de dados ✅ Totalmente compartilhado Dados puros, sem dependência de plataforma
Lógica de negócio ✅ Totalmente compartilhado Regras centrais devem ser consistentes
Rede ✅ Interface compartilhada + expect HTTP Interface unificada, implementações diferem
Serialização ✅ Compartilhado (kotlinx.serialization) Suporte nativo a múltiplos formatos
UI ❌ Específico da plataforma Frameworks de UI diferem significativamente
Banco de dados ⚠️ SQL compartilhado / expect SQL pode ser compartilhado, drivers diferem
Logging ⚠️ expect/actual APIs de logging por plataforma diferem

(2) Padrões de Estratégia Compartilhada

KOTLIN
// Padrão 1: Código puramente compartilhado (sem expect/actual necessário)
data class Order(val id: String, val total: Double, val status: String)

class OrderValidator {
    fun validate(order: Order): List<String> = buildList {
        if (!order.id.startsWith("ORD-")) add("Invalid ID format")
        if (order.total < 0) add("Negative total")
        if (order.status !in validStatuses) add("Invalid status")
    }

    companion object {
        private val validStatuses = setOf("PENDING", "CONFIRMED", "SHIPPED", "CANCELLED")
    }
}

// Padrão 2: Interface + fábrica expect
interface HttpClient {
    suspend fun get(url: String): String
    suspend fun post(url: String, body: String): String
}

expect fun createHttpClient(): HttpClient

// Padrão 3: Função expect para comportamento específico da plataforma
expect fun logDebug(tag: String, message: String)

6. Interoperabilidade

(1) Métodos de Interoperabilidade por Plataforma

Par de Plataformas Interoperabilidade Observações
JVM ↔ Java Bidirecional perfeita Kotlin chama Java diretamente; Java pode chamar Kotlin
iOS ↔ Swift Bidirecional Kotlin compila para framework Obj-C; Swift chama perfeitamente
JS ↔ JavaScript Bidirecional Função js() chama JS; @JsExport exporta Kotlin
Wasm ↔ JS Unidirecional (JS chama Kotlin) Módulo Wasm exporta funções para chamadas JS

(2) Exemplo de Interoperabilidade JVM

KOTLIN
// Kotlin chamando Java
val order = JavaOrderService()  // Classe Java
order.processOrder("ORD-001")   // Método Java

// Java chamando Kotlin (bytecode gerado é padrão)
// OrderKt.processOrder(order);  // Função de nível superior

(3) Exemplo de Interoperabilidade JS

KOTLIN
// Kotlin chamando JavaScript
fun fetchFromApi(url: String): dynamic {
    return js("fetch(url).then(r => r.json())")
}

// Exportar Kotlin para JavaScript
@JsExport
class OrderValidator {
    fun validate(id: String, total: Double): Boolean {
        return id.startsWith("ORD-") && total >= 0
    }
}

7. Exemplo Completo: OrderValidator Multiplataforma

KOTLIN
// ============================================
// OrderProcessor - Módulo Compartilhado KMP
// Funcionalidade: OrderValidator compartilhado com logging por plataforma
// ============================================

// --- commonMain ---

data class Order(val id: String, val total: Double, var status: String, val customer: String)

// expect: declaração específica da plataforma
expect fun logInfo(tag: String, message: String)

class OrderValidator {
    fun validate(order: Order): ValidationResult {
        val errors = mutableListOf<String>()

        if (!order.id.startsWith("ORD-")) errors.add("Invalid ID format: ${order.id}")
        if (order.total < 0) errors.add("Negative total: ${order.total}")
        if (order.total > 1_000_000) errors.add("Total exceeds maximum: ${order.total}")
        if (order.status !in VALID_STATUSES) errors.add("Invalid status: ${order.status}")

        val result = if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors)

        logInfo("OrderValidator", "Validated ${order.id}: $result")
        return result
    }

    fun calculatePriority(order: Order): String = when {
        order.total > 10_000 -> "HIGH"
        order.total > 1_000 -> "MEDIUM"
        else -> "LOW"
    }

    companion object {
        private val VALID_STATUSES = setOf("PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED")
    }
}

sealed class ValidationResult {
    object Valid : ValidationResult()
    data class Invalid(val errors: List<String>) : ValidationResult()
}

// --- jvmMain ---
// actual fun logInfo(tag: String, message: String) {
//     println("[$tag] $message")  // Ou use SLF4J
// }

// --- iosMain ---
// actual fun logInfo(tag: String, message: String) {
//     NSLog("$tag: $message")
// }

// --- jsMain ---
// actual fun logInfo(tag: String, message: String) {
//     console.log("[$tag] $message")
// }

// --- Demo (alvo JVM) ---
fun main() {
    // Simular actual JVM
    // actual fun logInfo(tag: String, message: String) = println("[$tag] $message")

    val validator = OrderValidator()

    val orders = listOf(
        Order("ORD-001", 299.99, "PENDING", "Alice"),
        Order("BAD-002", -50.0, "INVALID", "Bob"),
        Order("ORD-003", 15_000.00, "CONFIRMED", "Charlie"),
        Order("ORD-004", 2_000_000.00, "PENDING", "Dave")
    )

    println("=== KMP OrderValidator Demo ===")
    orders.forEach { order ->
        val result = validator.validate(order)
        val priority = validator.calculatePriority(order)
        when (result) {
            is ValidationResult.Valid -> println("  ✅ ${order.id}: Válido (Prioridade: $priority)")
            is ValidationResult.Invalid -> println("  ❌ ${order.id}: ${result.errors}")
        }
    }
}

Saída:

TEXT 📖 Somente leitura
=== KMP OrderValidator Demo ===
  ✅ ORD-001: Válido (Prioridade: LOW)
  ❌ BAD-002: [Invalid ID format: BAD-002, Negative total: -50.0, Invalid status: INVALID]
  ✅ ORD-003: Válido (Prioridade: HIGH)
  ❌ ORD-004: [Total exceeds maximum: 2000000.0]

9. Exemplos práticos rápidos

▶ Exemplo: Estrutura de projeto KMP

KOTLIN
// build.gradle.kts (root)
plugins {
    kotlin("multiplatform") version "1.9.0"
}

kotlin {
    iosX64()
    iosArm64()
    jvm()
    
    sourceSets {
        val commonMain by getting
        val commonTest by getting { dependencies { kotlinTest {} } }
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosMain by creating { dependsOn(commonMain) }
        val jvmMain by getting { dependsOn(commonMain) }
        val jvmTest by getting { dependsOn(commonTest) }
    }
}

▶ Exemplo: Código commonMain

KOTLIN
// commonMain/.../Platform.kt
// expect: declaração que cada plataforma deve fornecer
expect fun getPlatformName(): String

// commonMain/.../OrderProcessor.kt
data class Order(val id: String, val total: Double)

class OrderProcessor {
    fun process(order: Order): String {
        return "[${getPlatformName()}] Processando ${order.id}"
    }
}

▶ Exemplo: Platform-specific implementations

KOTLIN
// androidMain/.../Platform.kt
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

// iosMain/.../Platform.kt
import platform.UIKit.UIDevice
actual fun getPlatformName(): String {
    val device = UIDevice.currentDevice
    return "iOS ${device.systemVersion}"
}

// jvmMain/.../Platform.kt
actual fun getPlatformName(): String = "JVM ${System.getProperty("java.version")}"

// Uso
val processor = OrderProcessor()
println(processor.process(Order("ORD-001", 299.99)))
// Saída varia: "[Android 33]...", "[iOS 17.2]...", "[JVM 17.0.7]..."

▶ Exemplo: expect/actual para APIs dependentes

KOTLIN
// commonMain - declare API
expect class FileSystem {
    fun read(path: String): String
    fun write(path: String, content: String)
}

expect fun currentTimeMillis(): Long

// jvmMain
actual class FileSystem {
    actual fun read(path: String) = java.io.File(path).readText()
    actual fun write(path: String, content: String) {
        java.io.File(path).writeText(content)
    }
}

actual fun currentTimeMillis() = System.currentTimeMillis()

// iosMain
import platform.Foundation.*

actual class FileSystem {
    actual fun read(path: String): String {
        return NSString.stringWithContentsOfFile(path, encoding = NSUTF8StringEncoding, error = null) ?: ""
    }

    actual fun write(path: String, content: String) {
        NSString.stringWithString(content).writeToFile(path, atomically = true, encoding = NSUTF8StringEncoding, error = null)
    }
}

actual fun currentTimeMillis() = NSDate.timeIntervalSinceReferenceDate.toLong() * 1000

▶ Exemplo: Compartilhando lógica de negócios

KOTLIN
// commonMain - business logic puro
class PricingCalculator {
    fun applyDiscount(subtotal: Double, tier: String): Double {
        val discount = when (tier) {
            "GOLD" -> 0.15
            "PLATINUM" -> 0.25
            else -> 0.0
        }
        return subtotal * (1 - discount)
    }

    fun calculateTax(amount: Double) = amount * 0.08
}

// Usado em qualquer plataforma
val calc = PricingCalculator()
val original = 1000.0
val discounted = calc.applyDiscount(original, "PLATINUM")
val withTax = calc.calculateTax(discounted) + discounted
println("Original: \$$original")
println("Com desconto 25%: \$$discounted")
println("Com imposto: \$$withTax")

// Saída:
// Original: $1000.0
// Com desconto 25%: $750.0
// Com imposto: $810.0
KOTLIN
// Usando kotlinx-datetime em commonMain
import kotlinx.datetime.*

fun formatDate(date: Instant): String {
    val local = date.toLocalDateTime(TimeZone.currentSystemDefault())
    return "${local.year}-${local.monthNumber.toString().padStart(2, '0')}-${local.dayOfMonth.toString().padStart(2, '0')}"
}

val now = Clock.System.now()
println("Data atual: ${formatDate(now)}")

// Adicionar dias
val dueDate = now.plus(7, DateTimeUnit.DAY, TimeZone.currentSystemDefault())
println("Vencimento: ${formatDate(dueDate)}")

// Saída:
// Data atual: 2026-07-28
// Vencimento: 2026-08-04

▶ Exemplo: Platform-agnostic networking

KOTLIN
// commonMain usando ktor
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

class ApiClient(private val baseUrl: String) {
    private val client = HttpClient()

    suspend fun fetchOrder(id: String): String {
        val response = client.get("$baseUrl/orders/$id")
        return response.bodyAsText()
    }
}

// Uso:
val api = ApiClient("https://api.example.com")
val orderJson = api.fetchOrder("ORD-001")
println(orderJson)

❓ Perguntas Frequentes

P: Qual a diferença entre KMP e Flutter? R: KMP compartilha lógica de negócio (UI nativa por plataforma); Flutter compartilha UI (renderização Skia). KMP é mais flexível (preservando a experiência de UI nativa); Flutter é mais unificado (UI única). Eles podem se complementar.

P: O KMP está pronto para produção? R: Alvos JVM e Android são totalmente estáveis; alvo iOS é estável; alvos JS e Wasm estão amadurecendo rapidamente. Empresas como Netflix, VMware e Cash App já o usam em larga escala em produção.

P: expect/actual pode ser usado para classes? R: Sim. expect class declara uma interface multiplataforma; actual class fornece implementações por plataforma. Construtor e assinaturas de métodos devem corresponder exatamente.

P: Como organizar módulos compartilhados e de plataforma? R: Coloque a lógica compartilhada em commonMain; isole diferenças de plataforma com expect/actual nas bordas. Evite código específico de plataforma em commonMain — use expect para abstraí-lo.

P: O KMP é amigável para desenvolvedores iOS? R: Muito amigável. KMP compila para um framework Obj-C que Swift chama perfeitamente. Desenvolvedores iOS só precisam se preocupar com o lado Swift — não precisam conhecer Kotlin.

P: Como é a velocidade de build do KMP? R: Compilação multi-alvo aumenta o tempo de build (cada alvo compila independentemente). Compilação incremental do Gradle e cache do compilador Kotlin ajudam a mitigar isso. Em CI/CD, builds paralelas por alvo são recomendadas.


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Crie um projeto KMP, defina Order em commonMain e use-o nos alvos JVM e JS. Dica: Plugin Gradle kotlin("multiplatform")
  2. Intermediário (⭐⭐): Use expect/actual para fazer getPlatformName() retornar valores diferentes na JVM e no JS. Dica: expect fun getPlatformName(): String + implementações actual
  3. Desafio (⭐⭐⭐): Implemente um cliente HTTP multiplataforma: defina a interface HttpClient em commonMain, implemente com java.net.URL em jvmMain e com a API fetch em jsMain. Dica: expect fun createHttpClient(): HttpClient

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%