Durante muito tempo, falar em persistência local no Kotlin Multiplatform significava cair quase sempre em SQLDelight, wrappers próprios ou soluções diferentes para cada plataforma. Em 2026, esse cenário mudou bastante: o Room amadureceu seu suporte a KMP e já virou uma alternativa concreta para times Android que querem compartilhar entidades, DAOs e parte importante da camada de dados com iOS e outras plataformas.
Neste artigo, vamos ver como o Room funciona no mundo multiplataforma, quando faz sentido adotá-lo, como configurar commonMain, qual o papel dos drivers SQLite e quais cuidados você precisa ter para não tratar KMP como “Android com nomes diferentes”.
Se você ainda está construindo a base do assunto, vale revisar antes nosso guia de Kotlin Multiplatform Mobile, o tutorial de KMP e o artigo sobre o futuro do Kotlin Multiplatform.
O que mudou para o Room no Kotlin Multiplatform?
A principal novidade é que o Room 2.7+ passou a oferecer suporte real para Kotlin Multiplatform, permitindo definir em commonMain boa parte da estrutura que antes ficava presa ao Android:
- entidades (
@Entity); - interfaces DAO (
@Dao); - definição do banco (
@Database); - queries compartilhadas;
- retorno reativo com
Flow.
Isso aproxima muito a experiência de quem já conhece Room no Android. Em vez de abandonar completamente o conhecimento acumulado do ecossistema Jetpack, você reaproveita conceitos, anotações e parte da ergonomia.
Para times brasileiros que já mantêm app Android em Kotlin e querem evoluir para iOS com mais compartilhamento, esse é um salto importante. É o mesmo tipo de ganho gradual que comentamos em Kotlin para Android e em comparações como Kotlin Multiplatform vs Flutter.
Quando vale usar Room em KMP?
A resposta curta é: quando seu time já tem afinidade com o ecossistema AndroidX e quer reduzir a distância entre Android e KMP.
Room em KMP faz mais sentido quando:
- a equipe já domina Room no Android;
- a camada de dados precisa ser compartilhada entre Android e iOS;
- você quer continuar usando entidades e DAOs com uma API familiar;
- o projeto já segue arquitetura com repositórios e
Flow; - existe interesse em reduzir soluções paralelas entre plataformas.
Talvez não seja a melhor escolha quando:
- o projeto já está consolidado com SQLDelight e sem dores reais;
- a equipe precisa de suporte muito específico fora do modelo Room;
- a base multiplataforma ainda é experimental e muda demais;
- você quer o mínimo possível de acoplamento com AndroidX.
Como quase toda decisão arquitetural, isso depende mais do contexto do que de hype. O mesmo raciocínio vale quando discutimos monólito modular com Kotlin e Spring ou gRPC com Kotlin: a ferramenta certa é a que reduz complexidade real.
Dependências e plugins
Um setup moderno de Room com KMP pode começar assim:
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.google.devtools.ksp")
id("androidx.room")
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.sqlite:sqlite-bundled:2.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
}
}
}
dependencies {
add("kspAndroid", "androidx.room:room-compiler:2.8.4")
add("kspIosX64", "androidx.room:room-compiler:2.8.4")
add("kspIosArm64", "androidx.room:room-compiler:2.8.4")
add("kspIosSimulatorArm64", "androidx.room:room-compiler:2.8.4")
}
room {
schemaDirectory("$projectDir/schemas")
}
Dois pontos merecem atenção:
- KSP por target — em KMP, você precisa configurar o compilador do Room para cada alvo relevante.
- Schema versionada — salvar schemas continua sendo uma boa prática para evolução do banco e revisão de migrações.
Se você já acompanha o tema de tooling, aproveite também nosso conteúdo sobre KSP em Kotlin e Gradle Version Catalog, porque ambos ajudam a manter essa configuração organizada.
Estruturando o banco em commonMain
Aqui está a grande virada conceitual: em vez de deixar entidade e DAO presas ao Android, você move o núcleo do banco para commonMain.
Entidade compartilhada
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "artigos")
data class ArtigoEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val slug: String,
val titulo: String,
val categoria: String,
val favorito: Boolean = false,
)
DAO compartilhado
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface ArtigoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun salvar(item: ArtigoEntity)
@Query("SELECT * FROM artigos ORDER BY titulo ASC")
fun listarTodos(): Flow<List<ArtigoEntity>>
@Query("SELECT * FROM artigos WHERE favorito = 1")
fun listarFavoritos(): Flow<List<ArtigoEntity>>
@Query("UPDATE artigos SET favorito = :favorito WHERE slug = :slug")
suspend fun atualizarFavorito(slug: String, favorito: Boolean)
}
Banco compartilhado
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.ConstructedBy
@Database(
entities = [ArtigoEntity::class],
version = 1,
exportSchema = true,
)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun artigoDao(): ArtigoDao
}
Essa API deixa a modelagem muito mais alinhada com o restante do código compartilhado. Para projetos que já usam Flow e camadas reativas, o encaixe é natural.
O papel do @ConstructedBy e do construtor esperado
Em KMP, o Room precisa de uma forma multiplataforma para inicializar a implementação gerada do banco. Por isso aparece o padrão com expect object:
import androidx.room.RoomDatabaseConstructor
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
Esse padrão parece estranho à primeira vista, mas segue a mesma lógica do expect/actual que você já encontra no ecossistema KMP. Se quiser reforçar esse conceito, vale passar pelo nosso glossário sobre expect/actual.
Drivers SQLite: por que eles importam tanto?
No Android puro, muita gente quase nunca pensa profundamente no driver do banco, porque a plataforma já entrega quase tudo de forma implícita. No KMP, isso muda. Você precisa escolher como o Room conversa com SQLite em cada alvo.
Opção recomendada: BundledSQLiteDriver
Na maioria dos casos, a melhor escolha é o BundledSQLiteDriver(), porque ele entrega comportamento mais consistente entre plataformas.
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.coroutines.Dispatchers
fun criarBanco(builder: androidx.room.RoomDatabase.Builder<AppDatabase>): AppDatabase {
return builder
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
Essa consistência é valiosa porque reduz diferenças inesperadas entre Android e iOS. Em times menores, isso economiza bastante tempo de investigação.
Alternativas por plataforma
Dependendo do cenário, você também pode trabalhar com drivers específicos, como:
AndroidSQLiteDriverno Android;NativeSQLiteDriverno iOS.
Mas, para a maior parte dos casos novos, o driver bundled tende a simplificar a vida.
Criando o builder por plataforma
Mesmo com o banco definido em commonMain, o caminho físico do arquivo ainda depende da plataforma. Por isso, o builder costuma ser criado localmente em cada target.
Android
import android.content.Context
import androidx.room.Room
fun provideDatabaseBuilder(context: Context): androidx.room.RoomDatabase.Builder<AppDatabase> {
val dbFile = context.applicationContext.getDatabasePath("kotlin_brasil.db")
return Room.databaseBuilder<AppDatabase>(
context = context.applicationContext,
name = dbFile.absolutePath,
)
}
iOS
import androidx.room.Room
import platform.Foundation.NSHomeDirectory
fun provideDatabaseBuilder(): androidx.room.RoomDatabase.Builder<AppDatabase> {
val dbPath = NSHomeDirectory() + "/Documents/kotlin_brasil.db"
return Room.databaseBuilder<AppDatabase>(
name = dbPath,
)
}
A ideia é simples: a estrutura do banco é compartilhada, mas o local do arquivo ainda é responsabilidade de cada ambiente.
Repositório compartilhado com Flow
Com o DAO em commonMain, seu repositório pode ficar bastante limpo:
import kotlinx.coroutines.flow.Flow
class ArtigoRepository(
private val dao: ArtigoDao,
) {
fun listarFavoritos(): Flow<List<ArtigoEntity>> {
return dao.listarFavoritos()
}
suspend fun salvarArtigo(slug: String, titulo: String, categoria: String) {
dao.salvar(
ArtigoEntity(
slug = slug,
titulo = titulo,
categoria = categoria,
)
)
}
suspend fun alternarFavorito(slug: String, favorito: Boolean) {
dao.atualizarFavorito(slug, favorito)
}
}
Se a sua UI Android usa Compose e a iOS usa bindings próprios, ambas podem consumir a mesma regra de persistência. Esse é justamente o tipo de compartilhamento que torna o KMP interessante no mundo real.
Diferenças importantes em relação ao Room Android clássico
Aqui está um ponto essencial: Room em KMP não é só o Room Android transportado de plataforma. Existem diferenças práticas.
1. Flow tende a ser a escolha natural
Em código compartilhado, Flow funciona melhor do que abordagens ligadas ao Android, como LiveData.
2. Algumas APIs Android-only não existem
Recursos específicos do Android podem não estar disponíveis da mesma forma no ambiente multiplataforma.
3. Queries cruas e transações exigem atenção
Dependendo do caso, você vai usar APIs específicas do modelo KMP, inclusive para acesso por conexões de leitura e escrita.
4. Testes precisam cobrir mais de um ambiente mental
Mesmo que você não rode todos os testes em todas as plataformas o tempo todo, vale validar que sua modelagem não assume detalhes exclusivos do Android.
Esse cuidado é parecido com o que discutimos em Compose Multiplatform 1.10.6 e em Kotlin Multiplatform vs React Native: o segredo é respeitar o que é compartilhável e o que continua sendo específico de plataforma.
Exemplo de uso na camada de apresentação
Vamos imaginar um ViewModel compartilhado para artigos favoritos:
class FavoritosViewModel(
private val repository: ArtigoRepository,
) : ViewModel() {
val favoritos = repository.listarFavoritos()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
fun alternarFavorito(slug: String, favorito: Boolean) {
viewModelScope.launch {
repository.alternarFavorito(slug, favorito)
}
}
}
Num app Android com Compose, isso encaixa muito bem com telas declarativas. Num projeto iOS, a mesma base compartilhada continua útil, desde que a camada de apresentação esteja preparada para consumir esse estado.
Boas práticas para usar Room em KMP
Se você está avaliando adoção, estas recomendações ajudam bastante:
1. Comece com uma feature simples
Não migre toda a persistência do app de uma vez. Pilote com favoritos, cache de artigos, onboarding ou configurações locais.
2. Prefira modelos pequenos e estáveis
Evite entidades gigantes logo no começo. Quanto menor a superfície, mais fácil validar o comportamento multiplataforma.
3. Padronize o uso de Flow
Se a estratégia da equipe já é reativa, manter essa consistência facilita integração com UI e testes.
4. Versione schemas desde o início
Mesmo em projeto novo, deixe a evolução do banco previsível.
5. Documente limites da camada compartilhada
Nem toda necessidade de banco precisa estar em commonMain. Às vezes é melhor compartilhar 80% com qualidade do que forçar 100% e aumentar atrito.
Room em KMP substitui SQLDelight?
Não necessariamente. O que mudou é que agora existe uma opção mais forte dentro do universo AndroidX. Em alguns times, isso vai tornar o Room a escolha natural. Em outros, SQLDelight continuará fazendo mais sentido.
O ganho real é ter escolha madura. E isso é ótimo para o ecossistema Kotlin em 2026.
Conclusão
O suporte do Room ao Kotlin Multiplatform é uma das evoluções mais práticas do ecossistema recente. Ele não transforma KMP em solução mágica, mas reduz bastante a fricção para equipes Android que querem compartilhar persistência local sem abandonar ferramentas familiares.
Se o seu time já domina Room, trabalha com Flow e está levando KMP mais a sério, estudar essa stack faz muito sentido agora. Para muita gente, ela representa o caminho mais confortável entre o mundo Android tradicional e uma arquitetura realmente multiplataforma.
E esse talvez seja o principal valor da novidade: permitir que a adoção de KMP aconteça de forma mais incremental, com menos reinvenção e mais reaproveitamento de conhecimento já consolidado. Para quem trabalha com persistência em outras linguagens, vale comparar como Rust usa SQLx e Diesel para acesso a banco com type safety em tempo de compilação — uma filosofia parecida com o que o Room oferece no ecossistema Kotlin.
Perguntas Frequentes
Room funciona em Kotlin Multiplatform de verdade?
Sim. As versões recentes do Room já oferecem suporte a KMP, permitindo compartilhar entidades, DAOs e definição do banco em commonMain, com builders específicos por plataforma.
Preciso usar KSP com Room no KMP?
Sim, na prática o KSP continua sendo parte importante do setup, e normalmente você precisa configurar os targets relevantes para geração de código.
Qual driver SQLite usar no Room multiplataforma?
Na maioria dos casos novos, o BundledSQLiteDriver() é a escolha mais simples e previsível, porque mantém o comportamento mais consistente entre plataformas.
Room em KMP é melhor que SQLDelight?
Não existe resposta universal. Para times já acostumados com AndroidX e Room, a adoção pode ser mais natural. Para outros cenários, SQLDelight ainda pode ser a melhor opção.
O banco fica 100% compartilhado entre Android e iOS?
A estrutura principal pode ser compartilhada, mas o builder e o caminho físico do arquivo continuam específicos por plataforma. Ou seja: o núcleo compartilha bastante, mas nem tudo é idêntico.