🌌 P13: TARDIS Panel (DataStore Preferences)
Bem-vindo à segunda etapa da Fase 7: Arquitetura Avançada & Navegação! 🌌
Neste projeto prático focado em preferências do sistema, você aprenderá a persistir dados simples de forma totalmente assíncrona, robusta e orientada a reatividade usando o novo Jetpack DataStore Preferences do Google (o substituto moderno do legado SharedPreferences). O tema do app será o painel de pilotagem da lendária TARDIS de Doctor Who. Salvando chaves como ligar som, ativar modo neon e coordenadas espaciais, você compreenderá como gerenciar estados que persistem mesmo após reiniciar o celular do usuário.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P12: Batcomputer
🎯 Objetivo do Projeto
Construir um app de configurações de ficção científica com as seguintes metas:
- Persistência Não-Bloqueante: Declarar e criar instâncias seguras de gravação no DataStore.
- Chaves Fortemente Tipadas: Mapear chaves lógicas de tipo Boolean e String para leitura rápida de dados.
- Coleta Assíncrona (Kotlin Flows): Ler as atualizações de configurações em tempo real na tela usando fluxos e convertendo-os em estados do Compose.
- Estilização Tardis: Reagir ao chaveamento visual alterando as cores neon e mudando as coordenadas espaciais ao clicar no motor de propulsão.
graph TD
A[Usuário altera chave switch na tela] --> B[Dispara Coroutine Scope em background]
B --> C[DataStore edita e grava nova chave no disco de forma assíncrona]
C --> D[Flow do DataStore escuta modificação]
D --> E[CollectAsState atualiza Compose UI de forma reativa]
E --> F[Painel da TARDIS reage mudando cores e comportamentos]
📖 Dicionário do Projeto
- Jetpack DataStore: A solução moderna de armazenamento do Android projetada para substituir o SharedPreferences. Baseada em Kotlin Coroutines e Flows, ela manipula a persistência de forma 100% assíncrona na thread secundária (evitando travamentos visuais de UI).
- PreferencesDataStore: Variante do DataStore que armazena dados em pares de chave-valor simples, excelente para flags, switches e pequenos textos de preferências de usuário.
- preferencesDataStore(name): Delegador de inicialização única do Kotlin que cria a instância do arquivo físico de preferências no disco.
- booleanPreferencesKey() / stringPreferencesKey(): Métodos especiais que retornam chaves lógicas de acesso tipadas para garantir a integridade dos dados durante a escrita e a leitura.
- context.dataStore.edit { … }: Bloco executor assíncrono usado para registrar alterações seguras no disco de armazenamento.
🛠️ Passo 1: Adicionando Dependências do DataStore (Gradle)
- Abra o arquivo
build.gradle (Module :app)e inclua a biblioteca correspondente:dependencies { // Jetpack DataStore Preferences implementation "androidx.datastore:datastore-preferences:1.0.0" ... }Clique em Sync Now.
🛠️ Passo 2: Inicializando o DataStore Globalmente
No Kotlin, criamos a instância do DataStore de forma global (fora da classe MainActivity) para garantir que tenhamos um único ponto de acesso de preferência e evitar conflitos de arquivos:
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore
val Context.dataStore by preferencesDataStore(name = "tardis_settings")
🧠 Passo 3: Escutando os Estados e Atualizando Dados
- Declare suas chaves do DataStore dentro do Compose:
val SOM_VIAGEM = booleanPreferencesKey("som_viagem") val MODO_NEON = booleanPreferencesKey("modo_neon") - Colete os fluxos de leitura e converta-os em estados Compose (
collectAsState):val modoNeonState = context.dataStore.data.map { preferences -> preferences[MODO_NEON] ?: false }.collectAsState(initial = false) - Para escrever dados na persistência, use a corrotina reativa no switch da tela:
Switch( checked = somViagemState.value, onCheckedChange = { checked -> coroutineScope.launch { context.dataStore.edit { preferences -> preferences[SOM_VIAGEM] = checked } } } )
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Histórico de Configurações: salve e liste as últimas configurações gravadas no DataStore, criando um pequeno histórico de alterações.
- Tema Persistido: adicione um
Switchde tema claro/escuro cujo estado seja salvo e restaurado via DataStore.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/tardis/MainActivity.kt
package br.com.curso.tardis
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
// 1. EXTENSÃO DATASTORE
val Context.dataStore by preferencesDataStore(name = "tardis_settings")
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current
// 2. CHAVES DE PREFERÊNCIA
val SOM_VIAGEM = booleanPreferencesKey("som_viagem")
val MODO_NEON = booleanPreferencesKey("modo_neon")
val COORDENADAS = stringPreferencesKey("coordenadas")
// 3. FLUXOS DE LEITURA
val somViagemState = context.dataStore.data.map { preferences ->
preferences[SOM_VIAGEM] ?: true
}.collectAsState(initial = true)
val modoNeonState = context.dataStore.data.map { preferences ->
preferences[MODO_NEON] ?: false
}.collectAsState(initial = false)
val coordenadasState = context.dataStore.data.map { preferences ->
preferences[COORDENADAS] ?: "Londres, 1963"
}.collectAsState(initial = "Londres, 1963")
val coroutineScope = rememberCoroutineScope()
TardisTheme(isNeon = modoNeonState.value) {
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("TARDIS PANEL v3.5", fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color(0xFF0D253F),
titleContentColor = Color.White
)
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(if (modoNeonState.value) Color(0xFF030E1B) else Color(0xFF101C2B))
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.05f)),
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.padding(24.dp)) {
Text("COORDENADAS ATUAIS", fontSize = 12.sp, color = Color.Gray)
Text(coordenadasState.value, fontSize = 24.sp, fontWeight = FontWeight.Black, color = Color(0xFF00E676))
}
}
Spacer(modifier = Modifier.height(32.dp))
// Som switch
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Sons de Motor Tardis", color = Color.White)
Switch(
checked = somViagemState.value,
onCheckedChange = { checked ->
coroutineScope.launch {
context.dataStore.edit { preferences ->
preferences[SOM_VIAGEM] = checked
}
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
// Neon switch
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Modo Neon de Painel", color = Color.White)
Switch(
checked = modoNeonState.value,
onCheckedChange = { checked ->
coroutineScope.launch {
context.dataStore.edit { preferences ->
preferences[MODO_NEON] = checked
}
}
}
)
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
coroutineScope.launch {
context.dataStore.edit { preferences ->
preferences[COORDENADAS] = listOf("Gallifrey", "Marte, 2059", "Skaro", "Terra, 2026").random()
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF00B0FF))
) {
Text("PILOTAR NO ESPAÇO-TEMPO")
}
}
}
}
}
}
}
@Composable
fun TardisTheme(isNeon: Boolean, content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = if (isNeon) Color(0xFF00E676) else Color(0xFF00B0FF),
background = Color(0xFF101C2B)
),
content = content
)
}