Kotlin Multiplatform Mobile, agora oficialmente chamado apenas de Kotlin Multiplatform (KMP), permite compartilhar lógica de negócio entre Android e iOS usando Kotlin. Diferente de frameworks cross-platform que compartilham também a UI, como Flutter ou React Native, o KMP adota uma abordagem pragmatica: você compartilha o código que faz sentido compartilhar – lógica de negócio, acesso a dados, validacoes – e mantém a interface nativa de cada plataforma. Neste guia, vamos configurar um projeto KMP do zero, entender a arquitetura, implementar modulos compartilhados e explorar as melhores práticas para projetos reais.

Por Que Kotlin Multiplatform

A proposta do KMP e diferente de outras solucoes cross-platform. Enquanto Flutter substitui a UI nativa por seu próprio engine de renderizacao e React Native usa uma ponte entre JavaScript e componentes nativos, o KMP compila diretamente para JVM no Android e para código nativo via Kotlin/Native no iOS. Isso significa performance nativa em ambas as plataformas sem camadas de abstração adicionais.

As vantagens incluem compartilhamento gradual de código, integração total com projetos existentes é a possibilidade de manter equipes Android e iOS trabalhando com suas ferramentas nativas para a UI.

Configurando o Projeto

O Kotlin Multiplatform Wizard (disponivel em kmp.jetbrains.com) gera a estrutura inicial. Um projeto KMP tipico possui tres modulos principais:

// settings.gradle.kts
rootProject.name = "MeuAppKMP"
include(":androidApp")
include(":iosApp")
include(":shared")

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    kotlin("plugin.serialization")
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }

    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")
                implementation("io.ktor:ktor-client-core:2.3.7")
                implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
                implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:2.3.7")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-darwin:2.3.7")
            }
        }
    }
}

Estrutura de Source Sets

O KMP utiliza source sets para organizar código compartilhado e código específico de cada plataforma:

// shared/src/commonMain/kotlin/
// Código Kotlin puro, compartilhado entre todas as plataformas

// shared/src/androidMain/kotlin/
// Código especifico do Android (pode usar APIs do Android SDK)

// shared/src/iosMain/kotlin/
// Código especifico do iOS (pode usar APIs do iOS via interop)

Expect e Actual

O mecanismo expect/actual permite declarar APIs no código comum e implementa-las em cada plataforma:

// commonMain - Declaracao
expect class PlatformInfo() {
    val nome: String
    val versao: String
}

// androidMain - Implementacao Android
actual class PlatformInfo actual constructor() {
    actual val nome: String = "Android"
    actual val versao: String = "${android.os.Build.VERSION.SDK_INT}"
}

// iosMain - Implementacao iOS
actual class PlatformInfo actual constructor() {
    actual val nome: String = UIDevice.currentDevice.systemName()
    actual val versao: String = UIDevice.currentDevice.systemVersion
}

Outro exemplo prático com armazenamento local:

// commonMain
expect class KeyValueStorage {
    fun getString(key: String, default: String = ""): String
    fun putString(key: String, value: String)
    fun clear()
}

// androidMain
actual class KeyValueStorage(private val context: Context) {
    private val prefs = context.getSharedPreferences(
        "app_prefs", Context.MODE_PRIVATE
    )

    actual fun getString(key: String, default: String): String {
        return prefs.getString(key, default) ?: default
    }

    actual fun putString(key: String, value: String) {
        prefs.edit().putString(key, value).apply()
    }

    actual fun clear() {
        prefs.edit().clear().apply()
    }
}

// iosMain
actual class KeyValueStorage {
    private val defaults = NSUserDefaults.standardUserDefaults

    actual fun getString(key: String, default: String): String {
        return defaults.stringForKey(key) ?: default
    }

    actual fun putString(key: String, value: String) {
        defaults.setObject(value, forKey = key)
    }

    actual fun clear() {
        val dicionario = defaults.dictionaryRepresentation()
        for (key in dicionario.keys) {
            defaults.removeObjectForKey(key as String)
        }
    }
}

Networking Compartilhado com Ktor

O Ktor Client funciona em ambas as plataformas, permitindo compartilhar toda a camada de rede:

// commonMain
class ApiClient {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                prettyPrint = true
            })
        }
    }

    suspend fun buscarProdutos(): List<ProdutoDto> {
        return httpClient.get("https://api.exemplo.com/produtos")
            .body()
    }

    suspend fun buscarProduto(id: Long): ProdutoDto {
        return httpClient.get("https://api.exemplo.com/produtos/$id")
            .body()
    }
}

@Serializable
data class ProdutoDto(
    val id: Long,
    val nome: String,
    val preco: Double,
    val descricao: String
)

Repository Compartilhado

O padrão Repository funciona perfeitamente no modulo compartilhado:

// commonMain
class ProdutoRepository(
    private val apiClient: ApiClient
) {
    private val _produtos = MutableStateFlow<List<ProdutoDto>>(emptyList())
    val produtos: StateFlow<List<ProdutoDto>> = _produtos.asStateFlow()

    suspend fun carregarProdutos(): Result<List<ProdutoDto>> {
        return try {
            val resultado = apiClient.buscarProdutos()
            _produtos.value = resultado
            Result.success(resultado)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Integração com o App Android

No Android, o modulo shared e consumido como uma dependência normal:

// androidApp/build.gradle.kts
dependencies {
    implementation(project(":shared"))
}

// Uso no Android
class ProdutoViewModel(
    private val repository: ProdutoRepository
) : ViewModel() {
    val produtos = repository.produtos
        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}

Integração com o App iOS

No iOS, o framework gerado e importado no Swift:

// No Xcode / Swift
import shared

class ProdutoViewModel: ObservableObject {
    @Published var produtos: [ProdutoDto] = []

    private let repository = ProdutoRepository(
        apiClient: ApiClient()
    )

    func carregarProdutos() {
        Task {
            let resultado = try await repository.carregarProdutos()
            // Atualizar estado
        }
    }
}

Boas Práticas com Kotlin Multiplatform

  • Compartilhe lógica, não UI: mantenha a interface nativa para melhor experiência do usuário em cada plataforma.
  • Use interfaces no código comum: defina contratos com interfaces e implemente com expect/actual apenas quando necessário.
  • Prefira bibliotecas multiplatform: Ktor, kotlinx-serialization, SQLDelight e Koin possuem suporte KMP nativo.
  • Teste no commonTest: escreva testes unitarios no source set compartilhado para maxima cobertura entre plataformas.
  • Adoção incremental: comece compartilhando uma camada pequena (modelos de dados, por exemplo) e expanda gradualmente.
  • Mantenha o modulo shared leve: evite dependências pesadas que aumentem o tamanho do framework iOS.

Erros Comuns e Armadilhas

  • Congelar objetos no Kotlin/Native: versões mais antigas exigiam que objetos compartilhados entre threads fossem “frozen”. A partir do novo gerenciador de memória, isso não e mais necessário, mas bibliotecas antigas podem ainda ter essa restricao.
  • Coroutines no iOS: o Swift não entende suspend nativamente. Use wrappers como SKIE ou KMP-NativeCoroutines para expor funções suspensas como Swift async/await.
  • Build times longos: projetos KMP podem ter builds mais demorados. Configure caching adequado e builds incrementais.
  • Ignorar diferencias de plataforma: nem toda funcionalidade pode ou deve ser compartilhada. Permissoes, notificacoes push e acesso a sensores geralmente são melhor tratados nativamente.
  • Dependências transitivas: bibliotecas Java puras não funcionam no iOS. Certifique-se de que todas as dependências do commonMain sejam multiplatform.

Conclusão e Próximos Passos

O Kotlin Multiplatform oferece uma abordagem equilibrada para o desenvolvimento multiplataforma, combinando compartilhamento de código com interfaces nativas. A tecnologia amadureceu significativamente e já e usada em producao por empresas como Netflix, Philips e Cash App. Para aprofundar seus conhecimentos, explore o Compose Multiplatform para compartilhar também a UI, estude SQLDelight para persistencia multiplatform e consulte os demais guias sobre arquitetura e testes aqui no Kotlin Brasil.