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
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.
Divergir a assinatura entre expect e actual: os parametros, tipos de retorno e modificadores de visibilidade devem corresponder exatamente.
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.
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.
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 typealiaspermite mapear uma declaracao expect para um tipo existente da plataforma. - Gradle Plugin: o plugin
kotlin-multiplatformconfigura 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.