Capítulo 14: Armazenamento com Jetpack Preferences DataStore e Migração
Capítulo 14: Armazenamento com Jetpack Preferences DataStore e Migração
Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose
🗺️ Mapa Conceitual do Pipeline Assíncrono do DataStore
flowchart TD
APP["ViewModel / PreferenciasRepository"] --> DS["Jetpack Preferences DataStore"]
subgraph READ_STREAM["Pipeline de Leitura Não-Bloqueante (Kotlin Flow)"]
DS -->|"Expõe data: Flow<Preferences>"| CATCH["catch { emit(emptyPreferences()) }"]
CATCH --> MAP["map { preferences[CHAVE] ?: PADRAO }"]
MAP --> UI_STATE["Emissão Direta no StateFlow da Interface"]
end
subgraph WRITE_TRANSACTION["Pipeline de Escrita Transacional e Atômica"]
APP -->|"dataStore.edit { prefs -> ... }"| MUTEX["Bloqueio Transacional Mutex em Dispatchers.IO"]
MUTEX --> ATOMIC_WRITE["Gravação em Arquivo Temporário"]
ATOMIC_WRITE --> RENAME["Renomeação Atômica do Sistema de Arquivos"]
RENAME --> NOTIFY["Notificação Reativa Automática para os Leitores de Flow"]
end
NOTIFY -.-> READ_STREAM
style APP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style DS fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
style READ_STREAM fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style WRITE_TRANSACTION fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style UI_STATE fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. O Fim da Era SharedPreferences e a Ascensão do DataStore
Durante os primeiros doze anos do Android, o SharedPreferences foi a solução padrão para armazenar pares chave-valor (como configurações de usuário, tema e tokens de sessão). No entanto, o SharedPreferences possuía falhas de design arquitetural graves:
- Bloqueio Fatal da Main Thread com
commit(): O métodocommit()sincronizava os dados diretamente na thread em que era chamado. Se invocado na Main Thread durante uma transição de tela, causava congelamentos de frames e ANRs. O métodoapply(), embora assíncrono, não oferecia tratamento de erros e bloqueava a Main Thread silenciosamente durante o cicloonStop()da Activity. - Parsing de XML Frágil em Memória: O
SharedPreferencesmantinha um mapa completo carregado em memória após ler um arquivo XML bruto do disco, sem garantias de consistência transacional contra desligamentos abruptos do aparelho. - A Engenharia do Jetpack DataStore: Construído do zero com Kotlin Coroutines e Flow, o DataStore opera exclusivamente de forma assíncrona, não-bloqueante e segura por transações. Todas as mutações via
edit {}são atômicas e gravadas de forma segura contra corrupção de disco.
📊 2. Comparativo Detalhado: SharedPreferences vs DataStore
| Dimensão Técnica | SharedPreferences (Legado / Descontinuado) | Preferences DataStore (Padrão Ouro Moderno) |
|---|---|---|
| Modelo de Threading | Bloqueante na Main Thread com commit() |
Totalmente assíncrono baseado em Coroutines e Flow |
| API de Observação | OnSharedPreferenceChangeListener frágil e propenso a leaks |
data: Flow<Preferences> reativo com lifecycle do Kotlin |
| Tratamento de Exceções | Nulo (falhas de I/O são engolidas silenciosamente) | Emite IOException capturável com o operador catch {} |
| Consistência de Dados | Risco de perda em desligamento do dispositivo | Gravação em dois passos via arquivo temporário com rename atômico |
| Migração Integrada | Manual e sujeita a bugs de conversão | Migração automática através de SharedPreferencesMigration |
💻 3. Laboratório Prático: Repositório de Preferências de Usuário com DataStore
O código a seguir apresenta a implementação de um repositório profissional para gerenciar o modo de tema escuro e credenciais de sessão do usuário:
package br.com.portal.mobile.android.cap14
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
// Extensão de Context para criar o DataStore como Singleton
val Context.dataStoreUsuario: DataStore<Preferences> by preferencesDataStore(name = "configuracoes_usuario")
class PreferenciasUsuarioRepository(private val context: Context) {
private object Chaves {
val TEMA_ESCURO = booleanPreferencesKey("chave_tema_escuro_ativo")
val TOKEN_AUTENTICACAO = stringPreferencesKey("chave_jwt_autenticacao")
}
// Leitura Reativa: Trata falhas de I/O e emite o valor padrão com segurança
val temaEscuroHabilitado: Flow<Boolean> = context.dataStoreUsuario.data
.catch { excecao ->
if (excecao is IOException) {
emit(emptyPreferences())
} else {
throw excecao
}
}
.map { preferencias ->
preferencias[Chaves.TEMA_ESCURO] ?: false
}
val tokenAcesso: Flow<String?> = context.dataStoreUsuario.data
.catch { excecao ->
if (excecao is IOException) emit(emptyPreferences()) else throw excecao
}
.map { preferencias ->
preferencias[Chaves.TOKEN_AUTENTICACAO]
}
// Escrita Transacional e Segura com Coroutines
suspend fun definirTemaEscuro(ativo: Boolean) {
context.dataStoreUsuario.edit { preferencias ->
preferencias[Chaves.TEMA_ESCURO] = ativo
}
}
suspend fun salvarTokenAutenticacao(token: String) {
context.dataStoreUsuario.edit { preferencias ->
preferencias[Chaves.TOKEN_AUTENTICACAO] = token
}
}
suspend fun limparSessao() {
context.dataStoreUsuario.edit { preferencias ->
preferencias.remove(Chaves.TOKEN_AUTENTICACAO)
}
}
}
⚠️ 4. Armadilhas Comuns no Uso do DataStore
- Instanciar Múltiplos DataStores para o Mesmo Arquivo: Criar mais de uma instância de
DataStoreapontando para o mesmo nome de arquivo quebra o mecanismo interno de locks de leitura/escrita, disparandoIllegalStateException. Sempre utilize o delegate de propriedade noContext(by preferencesDataStore) em nível de arquivo. - Utilizar
runBlockingpara Forçar Leitura Síncrona: InvocarrunBlocking { dataStore.data.first() }na Main Thread anula todos os benefícios de performance do DataStore, bloqueando o desenho de telas e aumentando o risco de ANR. - Não Tratar
IOExceptioncomcatch {}: Corrupções de leitura de disco ou falta de permissão lançamIOException. Omitir o operadorcatch {}fará com que o Flow quebre a execução e crashe o app do usuário.