SQLDelight é uma das escolhas mais pragmáticas para persistência local em projetos Kotlin Multiplatform. Em vez de tentar esconder o SQLite atrás de um ORM pesado, ele parte de uma ideia simples: você escreve SQL real em arquivos .sq, o plugin valida as queries e gera uma API Kotlin type-safe para Android, iOS, desktop ou outro target suportado.
Para quem já usa Room com Kotlin no Android, a mudança de mentalidade é importante. Room é excelente quando o app é Android-only e você quer integração direta com Jetpack. SQLDelight brilha quando a camada de dados precisa viver no módulo shared de um projeto Kotlin Multiplatform, principalmente quando Android e iOS devem compartilhar regras de cache, queries, migrations e testes.
Este guia mostra como pensar SQLDelight em 2026: onde ele entra na arquitetura KMP, como configurar o módulo compartilhado, como escrever queries, como observar dados com Flow e quais cuidados tomar antes de levar para produção.
Quando SQLDelight faz sentido
Use SQLDelight quando o produto precisa de persistência estruturada e a lógica de dados deve ser compartilhada entre plataformas. Exemplos comuns:
- app Android e iOS com cache offline de catálogo, tarefas, pedidos ou mensagens;
- camada de favoritos, histórico ou fila de sincronização no módulo compartilhado;
- produto que usa Ktor Client em
commonMaine precisa persistir respostas normalizadas; - arquitetura offline-first com Kotlin que não deve duplicar regras em Swift e Kotlin Android;
- app KMP que quer testar repository, migrations e queries sem depender da UI nativa.
Se o app é apenas Android, Room continua sendo uma ótima escolha. Se você só precisa salvar tema, onboarding, idioma ou filtros simples, DataStore Preferences é menor e mais direto. SQLDelight entra quando os dados têm tabelas, relacionamentos, busca, ordenação, paginação ou sincronização.
Como organizar no módulo shared
Em um projeto KMP moderno, mantenha SQLDelight dentro do módulo compartilhado. A aplicação Android e o app iOS apenas fornecem o driver específico de plataforma.
plugins {
kotlin("multiplatform")
id("com.android.library")
id("app.cash.sqldelight") version "2.0.2"
}
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("app.cash.sqldelight:runtime:2.0.2")
implementation("app.cash.sqldelight:coroutines-extensions:2.0.2")
}
}
val androidMain by getting {
dependencies {
implementation("app.cash.sqldelight:android-driver:2.0.2")
}
}
val iosMain by getting {
dependencies {
implementation("app.cash.sqldelight:native-driver:2.0.2")
}
}
}
}
sqldelight {
databases {
create("AppDatabase") {
packageName.set("br.dev.kotlin.shared.db")
}
}
}
Confira sempre as versões compatíveis com seu Kotlin, Gradle e Android Gradle Plugin. Em times maiores, centralize versões no version catalog para evitar que Android e iOS compilem contra combinações diferentes.
Criando o schema em arquivos .sq
SQLDelight usa arquivos .sq como fonte de verdade. Um arquivo Tarefa.sq pode definir tabela e queries ao mesmo tempo:
CREATE TABLE tarefa (
id INTEGER NOT NULL PRIMARY KEY,
titulo TEXT NOT NULL,
concluida INTEGER AS Boolean NOT NULL DEFAULT 0,
atualizada_em INTEGER NOT NULL
);
listarTodas:
SELECT * FROM tarefa
ORDER BY atualizada_em DESC;
buscarPorId:
SELECT * FROM tarefa
WHERE id = ?;
salvar:
INSERT OR REPLACE INTO tarefa(id, titulo, concluida, atualizada_em)
VALUES (?, ?, ?, ?);
marcarConcluida:
UPDATE tarefa
SET concluida = ?, atualizada_em = ?
WHERE id = ?;
O plugin gera código Kotlin para chamar essas queries. Isso reduz uma classe inteira de bugs: nome de coluna errado, tipo incompatível, parâmetro ausente e query quebrada aparecem em build, não só em produção.
Repository compartilhado com Flow
No commonMain, você pode expor dados como Flow e deixar cada plataforma reagir do seu jeito. Android coleta no ViewModel; iOS pode adaptar para Swift usando a estratégia de interop do projeto.
class TarefasRepository(
private val database: AppDatabase,
private val clock: Clock,
) {
fun observarTarefas(): Flow<List<Tarefa>> =
database.tarefaQueries
.listarTodas()
.asFlow()
.mapToList(Dispatchers.Default)
.map { linhas -> linhas.map { it.toDomain() } }
fun salvar(tarefa: Tarefa) {
database.tarefaQueries.salvar(
id = tarefa.id,
titulo = tarefa.titulo,
concluida = tarefa.concluida,
atualizada_em = clock.now().toEpochMilliseconds(),
)
}
}
Repare que o repository não sabe se está rodando no Android ou no iOS. Ele só conhece o banco gerado, modelos de domínio e regras do produto. Essa separação combina com MVVM em Kotlin e com módulos compartilhados focados em lógica, não em detalhes de tela.
Drivers por plataforma
O ponto específico de plataforma é a criação do driver. No Android, você usa AndroidSqliteDriver:
actual class DatabaseDriverFactory(
private val context: Context,
) {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
No iOS, use o driver nativo:
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver =
NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
Esse padrão expect/actual mantém a inicialização separada e permite que o código comum receba apenas AppDatabase(driver).
Migrations sem improviso
SQLDelight valoriza migrations explícitas. Quando o schema evolui, crie arquivos como 1.sqm, 2.sqm e mantenha a sequência versionada no repositório.
ALTER TABLE tarefa ADD COLUMN prioridade INTEGER NOT NULL DEFAULT 0;
CREATE INDEX tarefa_prioridade_idx ON tarefa(prioridade);
Evite apagar banco em desenvolvimento como solução padrão. Isso esconde problemas que usuários reais terão ao atualizar o app. Teste migration com dados representativos, principalmente quando há filas offline, IDs temporários, relacionamentos ou campos usados por sincronização.
SQLDelight em arquitetura offline-first
Em uma arquitetura offline-first, SQLDelight normalmente guarda três tipos de dados:
- entidades exibidas na UI, como tarefas, produtos, mensagens ou pedidos;
- metadados de sincronização, como
updatedAt,syncStatusedeletedPendingSync; - fila de operações pendentes para enviar ao backend quando houver rede.
O repository lê primeiro do banco local, grava mudanças localmente e agenda sincronização com uma camada específica de plataforma, como WorkManager no Android. O importante é que a regra de negócio continue no módulo compartilhado. Assim, Android e iOS tomam as mesmas decisões sobre conflito, retry e merge.
Armadilhas comuns
A primeira armadilha é tratar SQLDelight como se fosse Room. Em SQLDelight, SQL é parte central do design. Use isso a favor do projeto: escreva queries claras, índices explícitos e migrations revisáveis.
A segunda é compartilhar demais. Nem todo detalhe precisa ir para commonMain. Se uma tela iOS usa cache temporário próprio ou se Android depende de API específica do Jetpack, mantenha essa decisão na plataforma. Compartilhe o domínio que realmente precisa ser consistente.
A terceira é ignorar observabilidade. Bugs de persistência local são difíceis de reproduzir. Registre versão do schema, estado de sincronização e falhas de migration em logs seguros. Para apps críticos, adicione métricas de fila pendente e tempo desde a última sincronização.
Próximos passos
Se você está começando agora, implemente um banco pequeno com duas tabelas, uma migration e um repository compartilhado. Depois conecte esse repository a uma tela Android e a uma tela iOS simples. Esse exercício ensina mais do que copiar uma arquitetura grande pronta.
Para aprofundar, combine este guia com a comparação Room vs SQLDelight, o guia de Kotlin Multiplatform Mobile, a trilha de Android offline-first e o conteúdo sobre KMP em 2026. Juntos, eles formam uma base sólida para entregar persistência local compartilhada sem sacrificar qualidade nativa.