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:

  1. 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.
  2. 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.
  3. 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) ou AutoMigration), 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

  1. 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 como suspend ou use Flow<T>, garantindo a execução em Dispatchers.IO.
  2. 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.
  3. Falta de Índices em Colunas de Busca: Executar SELECT filtrando por colunas de texto sem declarar indices = [Index("email")] na @Entity força um escaneamento sequencial completo de tabela (Full Table Scan), tornando a rolagem lenta quando o banco atinge milhares de registros.