O que é expect/actual em Kotlin?

As palavras-chave expect e actual são o mecanismo do Kotlin Multiplatform (KMP) para declarar APIs no código comum e fornecer implementacoes específicas para cada plataforma. O expect define o contrato no modulo compartilhado, é o actual fornece a implementação concreta em cada plataforma (JVM, iOS, JavaScript, etc.).

E como um contrato: o modulo comum diz “eu preciso desta funcionalidade”, e cada plataforma responde “aqui esta como eu implemento”.

O problema que expect/actual resolve

Quando você escreve código multiplataforma, a maioria da lógica pode ser compartilhada. Porem, algumas funcionalidades dependem da plataforma: acesso ao sistema de arquivos, criptografia, formatação de datas, acesso a rede nativa. O expect/actual permite que o código comum use essas funcionalidades sem conhecer a implementação específica.

Sintaxe básica

No modulo commonMain (código compartilhado):

// commonMain/src/Platform.kt
expect fun plataformaAtual(): String

expect class DataFormatada(timestamp: Long) {
    fun formatar(): String
}

No modulo jvmMain (implementação JVM):

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

actual class DataFormatada actual constructor(private val timestamp: Long) {
    actual fun formatar(): String {
        val sdf = java.text.SimpleDateFormat("dd/MM/yyyy HH:mm")
        return sdf.format(java.util.Date(timestamp))
    }
}

No modulo iosMain (implementação iOS):

// iosMain/src/Platform.kt
import platform.UIKit.UIDevice

actual fun plataformaAtual(): String =
    UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion

actual class DataFormatada actual constructor(private val timestamp: Long) {
    actual fun formatar(): String {
        val formatter = platform.Foundation.NSDateFormatter()
        formatter.dateFormat = "dd/MM/yyyy HH:mm"
        val date = platform.Foundation.NSDate(timeIntervalSince1970 = timestamp / 1000.0)
        return formatter.stringFromDate(date)
    }
}

O que pode ser expect/actual

Você pode usar expect/actual com vários tipos de declaracoes:

// Funções
expect fun gerarUUID(): String

// Classes
expect class HttpClient() {
    suspend fun get(url: String): String
}

// Objetos
expect object Configuração {
    val versao: String
    val debug: Boolean
}

// Propriedades top-level
expect val diretorioTemporario: String

// Anotações
expect annotation class Parcelize()

// Type aliases (actual pode ser typealias)
expect class BigDecimalMultiplatform
// No JVM:
actual typealias BigDecimalMultiplatform = java.math.BigDecimal

Exemplo completo: armazenamento de preferencias

Um caso de uso real e compartilhar a interface de armazenamento entre plataformas:

// commonMain
expect class PreferenciasStorage {
    fun salvar(chave: String, valor: String)
    fun ler(chave: String): String?
    fun remover(chave: String)
}

// Uso no codigo comum
class RepositorioDeConfiguracoes(private val storage: PreferenciasStorage) {
    fun salvarTema(tema: String) = storage.salvar("tema", tema)
    fun obterTema(): String = storage.ler("tema") ?: "claro"
}
// androidMain
import android.content.SharedPreferences

actual class PreferenciasStorage(private val prefs: SharedPreferences) {
    actual fun salvar(chave: String, valor: String) {
        prefs.edit().putString(chave, valor).apply()
    }
    actual fun ler(chave: String): String? = prefs.getString(chave, null)
    actual fun remover(chave: String) {
        prefs.edit().remove(chave).apply()
    }
}
// iosMain
import platform.Foundation.NSUserDefaults

actual class PreferenciasStorage {
    private val defaults = NSUserDefaults.standardUserDefaults

    actual fun salvar(chave: String, valor: String) {
        defaults.setObject(valor, forKey = chave)
    }
    actual fun ler(chave: String): String? = defaults.stringForKey(chave)
    actual fun remover(chave: String) {
        defaults.removeObjectForKey(chave)
    }
}

expect/actual com interfaces (alternativa moderna)

Em muitos casos, usar expect/actual com interfaces e injeção de dependências e mais flexivel:

// commonMain - definir interface
interface Plataforma {
    val nome: String
    fun gerarId(): String
}

// commonMain - declarar expect para obter a implementacao
expect fun criarPlataforma(): Plataforma

// jvmMain
actual fun criarPlataforma(): Plataforma = object : Plataforma {
    override val nome = "JVM"
    override fun gerarId() = java.util.UUID.randomUUID().toString()
}

// iosMain
actual fun criarPlataforma(): Plataforma = object : Plataforma {
    override val nome = "iOS"
    override fun gerarId() = platform.Foundation.NSUUID().UUIDString()
}

Essa abordagem combina o mecanismo expect/actual com polimorfismo, facilitando testes com implementacoes fake.

Configuração do projeto

A estrutura de diretorios de um projeto KMP com expect/actual tipicamente e:

src/
  commonMain/kotlin/    <- expect declarations
  commonTest/kotlin/    <- testes compartilhados
  jvmMain/kotlin/       <- actual JVM
  iosMain/kotlin/       <- actual iOS
  jsMain/kotlin/        <- actual JavaScript

Quando usar expect/actual

  • APIs de plataforma: quando você precisa acessar funcionalidades específicas do SO (arquivos, rede nativa, sensores).
  • Bibliotecas nativas: quando cada plataforma tem sua própria biblioteca para a mesma funcionalidade.
  • Otimizações específicas: quando a implementação ideal difere entre plataformas.
  • Integracoes de UI: quando componentes visuais são diferentes em cada plataforma.

Prefira código comum puro sempre que possível. Use expect/actual apenas quando realmente houver diferenca entre plataformas.

Casos de Uso no Mundo Real

  1. Armazenamento local multiplataforma: aplicações KMP usam expect/actual para abstrair o armazenamento de dados. No Android, a implementação actual usa SharedPreferences ou DataStore; no iOS, usa NSUserDefaults; na web, usa localStorage. O código comum define a interface de leitura e escrita sem conhecer os detalhes de cada plataforma.

  2. Criptografia e seguranca: bibliotecas multiplataforma de criptografia declaram funções expect para hashing, encriptacao e geracao de chaves. Cada plataforma fornece a implementação actual usando suas APIs nativas (java.security no JVM, CommonCrypto no iOS, Web Crypto API no JavaScript), garantindo performance e conformidade com padrões de seguranca nativos.

  3. Acesso a funcionalidades de hardware: aplicações que precisam acessar GPS, camera ou sensores declaram interfaces expect no código comum. As implementacoes actual em cada plataforma usam as APIs nativas correspondentes (LocationManager no Android, CLLocationManager no iOS), permitindo que a lógica de negócio permaneca no modulo compartilhado.

  4. Formatacao e internacionalizacao: a formatacao de datas, números e moedas depende de APIs especificas de cada plataforma. O expect/actual permite declarar funções de formatacao no commonMain e implementa-las com java.text.DateFormat no JVM, NSDateFormatter no iOS e Intl.DateTimeFormat no JavaScript.

Boas Praticas

  • Mantenha as declaracoes actual o mais finas possível, contendo apenas o código que realmente depende da plataforma. Toda lógica de negócio e validacao deve ficar no commonMain, onde pode ser testada uma única vez.
  • Prefira usar interfaces combinadas com funções factory expect/actual em vez de classes expect inteiras. Isso facilita a criação de implementacoes fake para testes e permite maior flexibilidade na injecao de dependências.
  • Use actual typealias quando a plataforma já possui um tipo que atende exatamente ao contrato do expect. Isso evita a criação de wrappers desnecessários e melhora a interoperabilidade com código nativo.
  • Escreva testes no commonTest que validem o contrato definido pelas declaracoes expect, e testes adicionais em cada source set de plataforma para verificar comportamentos específicos das implementacoes actual.
  • Avalie se a funcionalidade realmente precisa de expect/actual ou se pode ser resolvida com uma biblioteca multiplataforma existente (como kotlinx-datetime para datas ou Ktor para rede). Nem tudo que parece específico de plataforma requer implementação manual.

Perguntas Frequentes

P: O que acontece se eu esquecer de criar a implementação actual em uma plataforma? R: O compilador emite um erro durante a compilação da plataforma que não possui a implementação actual. O projeto não compila até que todas as declaracoes expect tenham seus correspondentes actual em cada plataforma alvo configurada no build.

P: Posso ter lógica diferente na implementação actual de cada plataforma? R: Sim, desde que a assinatura (nome, parametros, tipo de retorno e visibilidade) corresponda exatamente a declaracao expect. A implementação interna pode ser completamente diferente em cada plataforma. Essa e justamente a finalidade do mecanismo: mesmo contrato, implementacoes distintas.

P: Qual a diferenca entre usar expect/actual e interfaces com injecao de dependência? R: O expect/actual e resolvido em tempo de compilação pelo compilador Kotlin, enquanto interfaces com DI sao resolvidas em tempo de execução. O expect/actual e mais adequado para funções e tipos que precisam existir em cada plataforma sem overhead de runtime. Interfaces com DI sao mais flexiveis para testes e configuração dinâmica.

P: Posso usar expect/actual em projetos que não sao Kotlin Multiplatform? R: Nao. As palavras-chave expect e actual sao exclusivas do Kotlin Multiplatform e só funcionam em projetos configurados com o plugin kotlin-multiplatform. Em projetos single-platform, use interfaces e implementacoes convencionais.

Erros comuns

  1. Esquecer de implementar actual em alguma plataforma: o compilador reclama, mas o erro pode ser confuso se você tem muitos source sets. Verifique que toda declaracao expect tem um actual correspondente em cada plataforma alvo.

  2. Divergir a assinatura entre expect e actual: os parametros, tipos de retorno e modificadores de visibilidade devem corresponder exatamente.

  3. Usar expect/actual quando interfaces bastam: se a funcionalidade pode ser abstraida com uma interface e injeção de dependência, essa abordagem e mais testavel e flexivel.

  4. Colocar lógica de negócio no actual: o actual deve conter apenas o minimo necessário de código específico da plataforma. Logica de negócio pertence ao commonMain.

  5. Nao testar cada plataforma: testes no commonTest validam o contrato, mas você também precisa de testes em cada plataforma para garantir que as implementacoes actual funcionam corretamente.

Termos relacionados

  • Kotlin Multiplatform (KMP): o framework que permite compartilhar código Kotlin entre JVM, iOS, JavaScript e outras plataformas.
  • Source Set: conjunto de arquivos fonte específicos de uma plataforma ou compartilhados.
  • commonMain: source set do código compartilhado entre todas as plataformas.
  • Type Alias: actual typealias permite mapear uma declaracao expect para um tipo existente da plataforma.
  • Gradle Plugin: o plugin kotlin-multiplatform configura a compilação para múltiplas plataformas.
  • Serialization: a biblioteca kotlinx.serialization usa expect/actual internamente para serializar em diferentes plataformas.

O mecanismo expect/actual e a espinha dorsal do Kotlin Multiplatform, permitindo que você compartilhe a maior parte do código enquanto respeita as particularidades de cada plataforma. E um equilibrio elegante entre reutilização e especializacao.