Kotlin Multiplatform Mobile, agora oficialmente chamado apenas de Kotlin Multiplatform (KMP), permite compartilhar logica de negocio entre Android e iOS usando Kotlin. Diferente de frameworks cross-platform que compartilham tambem a UI, como Flutter ou React Native, o KMP adota uma abordagem pragmatica: voce compartilha o codigo que faz sentido compartilhar – logica de negocio, acesso a dados, validacoes – e mantem 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 praticas 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 proprio engine de renderizacao e React Native usa uma ponte entre JavaScript e componentes nativos, o KMP compila diretamente para JVM no Android e para codigo nativo via Kotlin/Native no iOS. Isso significa performance nativa em ambas as plataformas sem camadas de abstracao adicionais.

As vantagens incluem compartilhamento gradual de codigo, integracao total com projetos existentes e 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 codigo compartilhado e codigo especifico de cada plataforma:

// shared/src/commonMain/kotlin/
// Codigo Kotlin puro, compartilhado entre todas as plataformas

// shared/src/androidMain/kotlin/
// Codigo especifico do Android (pode usar APIs do Android SDK)

// shared/src/iosMain/kotlin/
// Codigo especifico do iOS (pode usar APIs do iOS via interop)

Expect e Actual

O mecanismo expect/actual permite declarar APIs no codigo 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 pratico 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 padrao 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)
        }
    }
}

Integracao com o App Android

No Android, o modulo shared e consumido como uma dependencia 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())
}

Integracao 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 Praticas com Kotlin Multiplatform

  • Compartilhe logica, nao UI: mantenha a interface nativa para melhor experiencia do usuario em cada plataforma.
  • Use interfaces no codigo comum: defina contratos com interfaces e implemente com expect/actual apenas quando necessario.
  • 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.
  • Adocao incremental: comece compartilhando uma camada pequena (modelos de dados, por exemplo) e expanda gradualmente.
  • Mantenha o modulo shared leve: evite dependencias pesadas que aumentem o tamanho do framework iOS.

Erros Comuns e Armadilhas

  • Congelar objetos no Kotlin/Native: versoes mais antigas exigiam que objetos compartilhados entre threads fossem “frozen”. A partir do novo gerenciador de memoria, isso nao e mais necessario, mas bibliotecas antigas podem ainda ter essa restricao.
  • Coroutines no iOS: o Swift nao entende suspend nativamente. Use wrappers como SKIE ou KMP-NativeCoroutines para expor funcoes 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 sao melhor tratados nativamente.
  • Dependencias transitivas: bibliotecas Java puras nao funcionam no iOS. Certifique-se de que todas as dependencias do commonMain sejam multiplatform.

Conclusao e Proximos Passos

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