O que e expect/actual em Kotlin?

As palavras-chave expect e actual sao o mecanismo do Kotlin Multiplatform (KMP) para declarar APIs no codigo comum e fornecer implementacoes especificas para cada plataforma. O expect define o contrato no modulo compartilhado, e o actual fornece a implementacao 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 voce escreve codigo multiplataforma, a maioria da logica pode ser compartilhada. Porem, algumas funcionalidades dependem da plataforma: acesso ao sistema de arquivos, criptografia, formatacao de datas, acesso a rede nativa. O expect/actual permite que o codigo comum use essas funcionalidades sem conhecer a implementacao especifica.

Sintaxe basica

No modulo commonMain (codigo compartilhado):

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

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

No modulo jvmMain (implementacao 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 (implementacao 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

Voce pode usar expect/actual com varios tipos de declaracoes:

// Funcoes
expect fun gerarUUID(): String

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

// Objetos
expect object Configuracao {
    val versao: String
    val debug: Boolean
}

// Propriedades top-level
expect val diretorioTemporario: String

// Anotacoes
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 injecao de dependencias 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.

Configuracao 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 voce precisa acessar funcionalidades especificas do SO (arquivos, rede nativa, sensores).
  • Bibliotecas nativas: quando cada plataforma tem sua propria biblioteca para a mesma funcionalidade.
  • Otimizacoes especificas: quando a implementacao ideal difere entre plataformas.
  • Integracoes de UI: quando componentes visuais sao diferentes em cada plataforma.

Prefira codigo comum puro sempre que possivel. Use expect/actual apenas quando realmente houver diferenca entre plataformas.

Erros comuns

  1. Esquecer de implementar actual em alguma plataforma: o compilador reclama, mas o erro pode ser confuso se voce 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 injecao de dependencia, essa abordagem e mais testavel e flexivel.

  4. Colocar logica de negocio no actual: o actual deve conter apenas o minimo necessario de codigo especifico da plataforma. Logica de negocio pertence ao commonMain.

  5. Nao testar cada plataforma: testes no commonTest validam o contrato, mas voce tambem precisa de testes em cada plataforma para garantir que as implementacoes actual funcionam corretamente.

Termos relacionados

  • Kotlin Multiplatform (KMP): o framework que permite compartilhar codigo Kotlin entre JVM, iOS, JavaScript e outras plataformas.
  • Source Set: conjunto de arquivos fonte especificos de uma plataforma ou compartilhados.
  • commonMain: source set do codigo 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 compilacao para multiplas 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 voce compartilhe a maior parte do codigo enquanto respeita as particularidades de cada plataforma. E um equilibrio elegante entre reutilizacao e especializacao.