Capítulo 13: Persistência Local com Room Database, SQLite e Reatividade
Capítulo 13: Persistência Local com Room Database, SQLite e Reatividade
Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose
🗺️ Mapa Conceitual da Arquitetura de Persistência com Room
flowchart TD
APP["Repositório / Casos de Uso (Kotlin Coroutines)"] --> DAO["Data Access Object (DAO Interface)"]
subgraph ROOM["Camada de Abstração do Room (KSP Code Generation)"]
DAO --> ROOM_DB["@Database (RoomDatabase Instance / Singleton)"]
ROOM_DB --> VALIDATE["Validação Estática de SQL em Tempo de Compilação"]
VALIDATE --> CONVERTERS["TypeConverters (Serialização JSON / Date / Enums)"]
end
subgraph SQLITE["Motor SQLite Nativo do Android (C / NDK)"]
ROOM_DB --> DRIVER["Android SQLite Driver"]
DRIVER --> DB_FILE["Arquivo no Disco Local (/data/data/pkg/databases/app.db)"]
end
DAO -->|"Retorna Flow<List<Entity>>"| REACTIVE["Emissão Reativa Imediata ao Gravar"]
REACTIVE --> APP
style APP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style DAO fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style ROOM_DB fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
style DB_FILE fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style REACTIVE fill:#fce4ec,stroke:#e91e63,stroke-width:2px
🏛️ 1. O Papel do Room na Camada de Dados Offline-First
No ecossistema Android móvel, a premissa de que o usuário sempre desfrutará de uma conexão com a internet rápida e estável é irreal. Aplicativos modernos seguem a filosofia Offline-First, onde o banco de dados local atua como a Fonte Única da Verdade (SSOT): os dados são imediatamente lidos e renderizados a partir do armazenamento local, enquanto rotinas de sincronização em segundo plano atualizam a base com o servidor remoto.
O Room Database substituiu os antigos SQLiteOpenHelper manuais e vulneráveis a injeções de SQL:
- Validação de Queries em Tempo de Compilação: Com o compilador KSP do Room, se você digitar incorretamente o nome de uma coluna ou tabela em uma anotação
@Query("SELECT nom FROM usuarios"), a compilação falha imediatamente, indicando a linha exata do erro sintático. - Reatividade Nativa com Kotlin Flow: Quando um método do DAO retorna
Flow<List<Entidade>>, o Room monitora ativamente as tabelas envolvidas na consulta. Se uma inserção, atualização ou deleção for efetuada em qualquer ponto do app, o Room re-executa a query no pool de threads I/O e emite a nova lista atualizada automaticamente para os assinantes. - Migrações Automáticas e Incrementais: Alterações no esquema de dados (como adicionar uma nova coluna de auditoria) são versionadas de forma controlada através de migrações (
Migration(1, 2)ouAutoMigration), prevenindo a perda irreversível de dados dos usuários durante atualizações de versão do app.
📊 2. Comparativo: SQLite Puro vs Room Database
| Aspecto Técnico | SQLiteOpenHelper Legado (Manual) | Room Database (Android Jetpack) |
|---|---|---|
| Checagem de Sintaxe SQL | Apenas em tempo de execução (risco de crashes) | Checagem estática no build via KSP |
| Mapeamento Objeto-Relacional | Mapeamento manual com Cursor e índices de coluna |
Conversão direta e transparente para data class |
| Integração com Coroutines | Exige gerenciamento manual de threads/handlers | Suporte nativo a suspend fun e Flow<T> |
| Tratamento de Tipos Complexos | Strings separadas por vírgula ou JSONs manuais | @TypeConverters com Kotlinx Serialization |
| Testabilidade do Banco | Difícil e demorada no hardware real | Testes rápidos em memória com Room.inMemoryDatabaseBuilder() |
💻 3. Laboratório Prático: Entidade, DAO Reativo e Configuração do Room
O código a seguir apresenta a declaração canônica de uma entidade com chave primária auto-gerada, um DAO com queries suspensas e reativas, e a classe do banco com @TypeConverters:
package br.com.portal.mobile.android.cap13
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import kotlinx.coroutines.flow.Flow
import java.util.Date
// 1. Definição da Entidade com Tabela e Índices
@Entity(tableName = "tabela_anotacoes")
data class AnotacaoEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val titulo: String,
val conteudo: String,
val dataCriacao: Date = Date(),
val concluida: Boolean = false
)
// 2. Data Access Object (DAO) Reativo e Não-Bloqueante
@Dao
interface AnotacaoDao {
@Query("SELECT * FROM tabela_anotacoes ORDER BY dataCriacao DESC")
fun observarTodas(): Flow<List<AnotacaoEntity>>
@Query("SELECT * FROM tabela_anotacoes WHERE id = :anotacaoId")
suspend fun buscarPorId(anotacaoId: Long): AnotacaoEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun salvar(anotacao: AnotacaoEntity): Long
@Delete
suspend fun excluir(anotacao: AnotacaoEntity)
}
// 3. Conversor de Tipos para Campos Complexos (Date <-> Long)
class DateTypeConverter {
@TypeConverter
fun deTimestamp(valor: Long?): Date? = valor?.let { Date(it) }
@TypeConverter
fun dateParaTimestamp(data: Date?): Long? = data?.time
}
// 4. Instância Abstrata do Banco de Dados
@Database(entities = [AnotacaoEntity::class], version = 1, exportSchema = false)
@TypeConverters(DateTypeConverter::class)
abstract class AppRoomDatabase : RoomDatabase() {
abstract fun anotacaoDao(): AnotacaoDao
}
⚠️ 4. Armadilhas Comuns no Uso do Room
- Acessar o Banco na Main Thread: O Room bloqueia intencionalmente qualquer consulta executada na thread de interface, lançando
IllegalStateException: Cannot access database on the main thread. Sempre declare seus métodos do DAO comosuspendou useFlow<T>, garantindo a execução emDispatchers.IO. fallbackToDestructiveMigration()em Produção: Ativar a destruição de dados em migrações (fallbackToDestructiveMigration()) apaga completamente todas as tabelas e dados salvos pelo usuário caso você incremente a versão sem fornecer uma classe de migração. Nunca use isso em releases enviadas para a Google Play Store.- Falta de Índices em Colunas de Busca: Executar
SELECTfiltrando por colunas de texto sem declararindices = [Index("email")]na@Entityforça um escaneamento sequencial completo de tabela (Full Table Scan), tornando a rolagem lenta quando o banco atinge milhares de registros.