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:

  1. KSP por target — em KMP, você precisa configurar o compilador do Room para cada alvo relevante.
  2. 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:

  • AndroidSQLiteDriver no Android;
  • NativeSQLiteDriver no 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.