DataStore Preferences é a alternativa moderna ao SharedPreferences para salvar configurações simples em apps Android com Kotlin. Ele resolve problemas comuns de leitura bloqueante, concorrência, callbacks difíceis de testar e integração fraca com arquitetura reativa. Em vez de chamar getString() espalhado pela aplicação, você expõe um Flow tipado, grava de forma assíncrona e mantém a camada de UI observando estado consistente.
O caso de uso ideal é pequeno e frequente: tema escolhido, filtros de lista, flags de onboarding, último usuário selecionado, timestamp da última sincronização, preferências de notificação ou toggles de recurso. Para dados relacionais, listas grandes, busca, paginação ou histórico, use Room com Kotlin. Para sincronização confiável em background, combine com WorkManager. DataStore entra no meio: simples como preferências, mas com ergonomia de Kotlin moderno.
Quando usar DataStore Preferences?
Use DataStore Preferences quando você precisa persistir pares chave-valor pequenos e observar mudanças no app inteiro. Ele é especialmente útil quando a preferência afeta várias telas, porque o Flow permite que ViewModels reajam automaticamente.
Bons exemplos:
- tema claro, escuro ou seguindo o sistema;
- idioma selecionado dentro do app;
- filtro padrão de uma lista;
- ordenação escolhida pelo usuário;
- flag de onboarding concluído;
- token de paginação simples;
- data da última sincronização bem-sucedida;
- preferências locais de notificações.
Evite DataStore para objetos grandes ou coleções que precisam de consulta. Se você quer filtrar tarefas por status, buscar por texto, relacionar entidades ou migrar esquema com várias tabelas, a escolha correta é Room. DataStore não é um banco de dados; ele é uma camada segura para preferências pequenas.
DataStore Preferences vs Proto DataStore
O Jetpack oferece duas formas principais de DataStore:
| Opção | Melhor para | Trade-off |
|---|---|---|
| Preferences DataStore | chaves simples, setup rápido, migração de SharedPreferences | não tem schema formal |
| Proto DataStore | modelo tipado com schema e evolução controlada | exige protobuf e mais configuração |
Para a maioria dos apps Android iniciando uma tela de configurações, Preferences DataStore é suficiente. Proto DataStore faz mais sentido quando a configuração vira um objeto de domínio importante, precisa de validação estrutural ou será compartilhada entre módulos com contrato explícito.
Neste guia vamos focar em Preferences DataStore, porque ele cobre o caminho mais comum para substituir SharedPreferences sem criar complexidade prematura.
Dependências
Adicione a dependência no módulo Android:
dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.1")
}
Se o projeto já usa Compose, Coroutines e Flow, nenhuma mudança conceitual grande é necessária. DataStore combina naturalmente com ViewModel e collectAsStateWithLifecycle.
Criando o DataStore
O padrão mais comum é criar uma extensão no Context usando o delegate preferencesDataStore:
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore
private val Context.userPreferencesDataStore by preferencesDataStore(
name = "user_preferences",
)
O nome deve ser estável. Trocar esse nome cria outro arquivo de preferências, então trate como parte da persistência do app. Em projetos grandes, mantenha a extensão em um módulo de dados ou infraestrutura, não dentro de uma Activity.
Modelando preferências tipadas
Mesmo usando Preferences DataStore, evite espalhar strings de chave pela aplicação. Defina chaves em um único lugar e exponha um modelo de domínio simples:
enum class TemaApp {
Sistema,
Claro,
Escuro,
}
data class UserPreferences(
val tema: TemaApp = TemaApp.Sistema,
val onboardingConcluido: Boolean = false,
val ordemFavoritos: String = "recentes",
)
Agora crie as chaves:
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
private object PreferenceKeys {
val Tema = stringPreferencesKey("tema")
val OnboardingConcluido = booleanPreferencesKey("onboarding_concluido")
val OrdemFavoritos = stringPreferencesKey("ordem_favoritos")
}
Essa pequena organização evita bugs bobos como usar theme em uma tela e tema em outra. Também deixa migrações e testes mais simples.
Repository para leitura com Flow
Crie uma classe responsável por transformar Preferences em um modelo da aplicação:
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>,
) {
val preferences: Flow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(androidx.datastore.preferences.core.emptyPreferences())
} else {
throw exception
}
}
.map { prefs ->
UserPreferences(
tema = prefs[PreferenceKeys.Tema]
?.let { runCatching { TemaApp.valueOf(it) }.getOrNull() }
?: TemaApp.Sistema,
onboardingConcluido = prefs[PreferenceKeys.OnboardingConcluido] ?: false,
ordemFavoritos = prefs[PreferenceKeys.OrdemFavoritos] ?: "recentes",
)
}
}
O catch acima trata erro de leitura do arquivo como fallback para preferências vazias. Não engula qualquer exceção. Se o problema for programação incorreta, corrupção não tratada ou outro erro inesperado, deixar a exceção aparecer ajuda a corrigir o bug.
Gravando preferências
Para escrever, use edit. A operação é suspensa e transacional para o arquivo de preferências:
import androidx.datastore.preferences.core.edit
suspend fun definirTema(tema: TemaApp) {
dataStore.edit { prefs ->
prefs[PreferenceKeys.Tema] = tema.name
}
}
suspend fun concluirOnboarding() {
dataStore.edit { prefs ->
prefs[PreferenceKeys.OnboardingConcluido] = true
}
}
suspend fun definirOrdemFavoritos(ordem: String) {
dataStore.edit { prefs ->
prefs[PreferenceKeys.OrdemFavoritos] = ordem
}
}
Não faça escrita direta na UI. A tela chama um método do ViewModel, o ViewModel chama o repository e o estado volta pelo Flow. Esse fluxo unidirecional evita inconsistência entre “o botão foi tocado” e “a preferência realmente foi persistida”.
Usando no ViewModel
No ViewModel, exponha o fluxo como estado de tela:
class SettingsViewModel(
private val repository: UserPreferencesRepository,
) : ViewModel() {
val uiState: StateFlow<UserPreferences> = repository.preferences
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UserPreferences(),
)
fun selecionarTema(tema: TemaApp) {
viewModelScope.launch {
repository.definirTema(tema)
}
}
}
Se você já usa MVVM com Kotlin, esse desenho deve parecer familiar: a UI observa estado, o ViewModel expõe ações e o repository cuida da persistência.
Consumindo em Compose
Em Jetpack Compose, a tela pode observar o estado com lifecycle-aware collection:
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel,
) {
val preferences by viewModel.uiState.collectAsStateWithLifecycle()
Column {
Text("Tema")
TemaApp.entries.forEach { tema ->
Row(
modifier = Modifier.clickable {
viewModel.selecionarTema(tema)
},
) {
RadioButton(
selected = preferences.tema == tema,
onClick = { viewModel.selecionarTema(tema) },
)
Text(text = tema.name)
}
}
}
}
Para aprofundar a camada visual, veja também o guia de Jetpack Compose e os tutoriais de layouts. DataStore não deve conhecer Compose; ele apenas fornece estado.
Migração de SharedPreferences
Se o app já usa SharedPreferences, DataStore oferece migração. Isso é importante porque trocar a implementação sem migrar pode resetar tema, filtros e flags do usuário.
private val Context.userPreferencesDataStore by preferencesDataStore(
name = "user_preferences",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = "legacy_user_preferences",
),
)
},
)
Antes de remover o código antigo, valide em um build com dados reais ou fixture de QA. A migração deve preservar os nomes das chaves relevantes ou mapear valores quando o formato mudou. Se antes você salvava "dark" e agora usa TemaApp.Escuro.name, trate essa conversão explicitamente.
DataStore em arquitetura offline-first
Em apps offline-first, DataStore costuma guardar metadados pequenos enquanto Room guarda o estado principal. Um exemplo comum:
- Room: entidades, filas pendentes, dados exibidos na tela;
- DataStore: último sync bem-sucedido, filtro escolhido, usuário ativo, feature flag local;
- WorkManager: execução de sincronização quando houver rede;
- Repository: regra que combina tudo isso.
Esse desenho evita dois extremos ruins: colocar preferências simples no banco só por formalidade ou enfiar dados de produto dentro de DataStore porque é mais rápido de começar. Cada ferramenta resolve um problema.
Se a sua tela offline-first precisa mostrar “última atualização: 10:32”, DataStore pode guardar esse timestamp. Se precisa listar 300 pedidos pendentes, use Room.
Testes
O repository de DataStore é fácil de testar quando você injeta DataStore<Preferences> em vez de buscar Context diretamente dentro da classe. Em testes instrumentados ou Robolectric, crie um arquivo temporário e valide leitura/escrita.
O que vale testar:
- valor padrão quando não existe preferência salva;
- escrita de tema atualiza o fluxo observado;
- valores antigos de SharedPreferences são migrados;
- enum inválido cai para valor seguro;
- erro de leitura esperado não derruba a tela;
- ViewModel chama o repository em uma ação de UI.
Para testes unitários de lógica ao redor, combine com JUnit 5 e MockK. Para fluxos, Turbine também é uma boa ferramenta, especialmente quando você precisa validar sequência de emissões.
Erros comuns
O primeiro erro é usar DataStore como banco. Quando preferências começam a virar JSON grande, lista serializada ou mapa complexo, provavelmente você está escondendo uma entidade que deveria estar no Room.
O segundo erro é ler DataStore de forma pontual em todo lugar. O modelo idiomático é observar Flow e transformar em estado. Se cada tela cria sua própria leitura avulsa, o app fica difícil de testar.
O terceiro erro é salvar strings livres sem fallback. Enums mudam, valores antigos continuam no dispositivo e usuários atualizam de versões antigas. Sempre trate valor desconhecido.
O quarto erro é esquecer que preferências também fazem parte da experiência. Resetar tema, idioma, onboarding ou filtros em uma atualização passa sensação de app quebrado. Migração deve entrar no checklist de release.
Checklist prático
Antes de usar DataStore Preferences em produção, confirme:
- as chaves ficam centralizadas;
- o repository expõe um modelo tipado;
- a UI observa estado via ViewModel;
- valores padrão são explícitos;
- enums têm fallback seguro;
- SharedPreferences legado foi migrado quando necessário;
- dados grandes continuam no Room;
- testes cobrem leitura, escrita e migração.
Conclusão
DataStore Preferences é uma melhoria direta para apps Android Kotlin que ainda dependem de SharedPreferences. Ele combina persistência simples, coroutines, Flow e uma API mais segura para arquitetura moderna. O ganho não é apenas técnico: preferências reativas deixam a UI consistente, reduzem leituras bloqueantes e tornam testes mais previsíveis.
Use DataStore para configurações pequenas, Room para dados estruturados e WorkManager para trabalho persistente em background. Essa combinação forma uma base forte para apps Android reais em 2026: responsivos, offline-aware e fáceis de evoluir. Para quem está montando portfólio, uma tela de configurações com DataStore bem organizado mostra maturidade além do CRUD básico.