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:

  1. Bloqueio Fatal da Main Thread com commit(): O método commit() 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étodo apply(), embora assíncrono, não oferecia tratamento de erros e bloqueava a Main Thread silenciosamente durante o ciclo onStop() da Activity.
  2. Parsing de XML Frágil em Memória: O SharedPreferences mantinha 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.
  3. 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

  1. Instanciar Múltiplos DataStores para o Mesmo Arquivo: Criar mais de uma instância de DataStore apontando para o mesmo nome de arquivo quebra o mecanismo interno de locks de leitura/escrita, disparando IllegalStateException. Sempre utilize o delegate de propriedade no Context (by preferencesDataStore) em nível de arquivo.
  2. Utilizar runBlocking para Forçar Leitura Síncrona: Invocar runBlocking { 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.
  3. Não Tratar IOException com catch {}: Corrupções de leitura de disco ou falta de permissão lançam IOException. Omitir o operador catch {} fará com que o Flow quebre a execução e crashe o app do usuário.