📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Bem-vindo à segunda etapa da Fase 7: Arquitetura Avançada & Persistência! 🌌
Neste projeto prático, você dominará o padrão moderno e recomendado pelo Google para persistência leve de configurações no Android: o Jetpack DataStore Preferences (o substituto definitivo do legado e síncrono SharedPreferences).
Construiremos o painel de pilotagem da lendária TARDIS de Doctor Who. O aplicativo gerencia e persiste chaves de estado essenciais — como efeitos sonoros dos motores, modo de iluminação neon quântica, escudos temporais e as coordenadas de destino no espaço-tempo. Todos os dados são salvos de forma 100% assíncrona, protegidos na sandbox interna do dispositivo e integrados reativamente ao Jetpack Compose através de Kotlin Flows!
| 1. Painel Clássico Azul | 2. Modo Neon Quântico | 3. Salto Espaço-Temporal |
|---|---|---|
| Tema clássico com coordenadas e switches de controle. | Transição de tema dinâmica com animateColorAsState. |
Gravação assíncrona no DataStore com feedback Toast. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
preferencesDataStore(name = "tardis_settings").booleanPreferencesKey() e stringPreferencesKey() para evitar erros de digitação e corrupção de tipos.dataStore.data.map { ... } e convertê-lo em estados observáveis do Compose com collectAsState().context.dataStore.edit { ... } dentro de coroutineScope.launch sem travar a Thread principal da UI (Main Thread).animateColorAsState para alternar suavemente entre a paleta clássica e o modo neon fluorescente da TARDIS.graph TD
A[Usuário altera Switch ou clica em MATERIALIZAR] --> B[Dispara Coroutine com coroutineScope.launch]
B --> C[DataStore: context.tardisDataStore.edit]
C --> D[Gravação Assíncrona no Disco na Sandbox do App]
D --> E[Flow reativo emite nova emissão de Preferences]
E --> F[dataStore.data.map transforma o valor na chave desejada]
F --> G[collectAsState notifica o Jetpack Compose]
G --> H[Recomposição da UI com novo Tema e Coordenadas]
[!IMPORTANT] Por que o Google substituiu o
SharedPreferencespeloDataStore Preferences?
- Segurança contra Travamentos (ANR - Application Not Responding): O
SharedPreferencesantigo permitia chamadas síncronas (commit()) na Main Thread, causando lentidão e travamentos. O DataStore utiliza Coroutines e I/O não-bloqueante por padrão.- Isolamento na Sandbox: O DataStore grava o arquivo
tardis_settings.preferences_pbno diretório privado da aplicação (/data/data/br.com.curso.tardis/files/datastore/), inacessível para outros aplicativos instalados no celular.- Segurança de Tipos (Type-Safety): Não há risco de
ClassCastExceptionem tempo de execução, pois as chaves são explicitamente tipadas viabooleanPreferencesKeyoustringPreferencesKey.- Higiene de Versionamento: O arquivo
.gitignorefoi configurado para garantir que arquivos temporários e chaves locais nunca sejam commitados.
| Termo / Componente | O que é e para que serve? |
|---|---|
| Jetpack DataStore Preferences | Biblioteca moderna de persistência chave-valor do Android baseada em Protocol Buffers, Kotlin Coroutines e Flows. |
| preferencesDataStore(name) | Delegado de inicialização única no Kotlin que gerencia o arquivo de armazenamento no disco. |
| booleanPreferencesKey / stringPreferencesKey | Fábricas que criam chaves de acesso com verificação de tipo em tempo de compilação. |
| context.dataStore.edit | Função transacional que garante escritas atômicas e seguras no disco de armazenamento. |
| Flow.collectAsState | Utilitário do Compose que escuta as emissões do fluxo assíncrono e converte em um State<T> reativo. |
| animateColorAsState | Animação automática do Compose que interpola cores suavemente quando o estado do tema é alterado. |
File > New > New Project).TARDIS Panel (ou android_p13_tardis_services)br.com.curso.tardisKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)build.gradle)Abra app > build.gradle e adicione a dependência do Jetpack DataStore Preferences:
dependencies {
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
// Jetpack DataStore Preferences (Persistência Reativa e Assíncrona)
implementation 'androidx.datastore:datastore-preferences:1.0.0'
}
Clique em Sync Now.
AndroidManifest.xml)Abra app > src > main > AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="TARDIS Panel"
android:supportsRtl="true"
android:theme="@style/Theme.CartaoTreinador">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
No topo do arquivo app/src/main/java/br/com/curso/tardis/MainActivity.kt:
// Extensão singleton com escopo de Contexto
val Context.tardisDataStore by preferencesDataStore(name = "tardis_settings")
// Chaves tipadas
val SOM_VIAGEM_KEY = booleanPreferencesKey("som_viagem")
val MODO_NEON_KEY = booleanPreferencesKey("modo_neon")
val ESCUDO_TEMPORAL_KEY = booleanPreferencesKey("escudo_temporal")
val COORDENADAS_KEY = stringPreferencesKey("coordenadas")
val PILOTO_NOME_KEY = stringPreferencesKey("piloto_nome")
TardisTheme)@Composable
fun TardisTheme(isNeon: Boolean, content: @Composable () -> Unit) {
val primaryColor by animateColorAsState(
targetValue = if (isNeon) Color(0xFF00E676) else Color(0xFF00B0FF),
label = "ThemePrimary"
)
val secondaryColor by animateColorAsState(
targetValue = if (isNeon) Color(0xFF00B0FF) else Color(0xFF9C27B0),
label = "ThemeSecondary"
)
MaterialTheme(
colorScheme = darkColorScheme(
primary = primaryColor,
secondary = secondaryColor,
background = if (isNeon) Color(0xFF04101E) else Color(0xFF091424),
surface = if (isNeon) Color(0xFF0A1D36) else Color(0xFF10233D),
surfaceVariant = Color(0xFF183354)
),
content = content
)
}
@Composable
fun TardisRootApp() {
val context = LocalContext.current
// Fluxos reativos do DataStore convertidos em State
val somViagem by context.tardisDataStore.data.map { it[SOM_VIAGEM_KEY] ?: true }.collectAsState(initial = true)
val modoNeon by context.tardisDataStore.data.map { it[MODO_NEON_KEY] ?: false }.collectAsState(initial = false)
val escudoAtivo by context.tardisDataStore.data.map { it[ESCUDO_TEMPORAL_KEY] ?: true }.collectAsState(initial = true)
val coordenadas by context.tardisDataStore.data.map { it[COORDENADAS_KEY] ?: "Londres, 1963" }.collectAsState(initial = "Londres, 1963")
val pilotoNome by context.tardisDataStore.data.map { it[PILOTO_NOME_KEY] ?: "14º Doutor(a)" }.collectAsState(initial = "14º Doutor(a)")
TardisTheme(isNeon = modoNeon) {
TardisScreen(
somViagem = somViagem,
modoNeon = modoNeon,
escudoAtivo = escudoAtivo,
coordenadas = coordenadas,
pilotoNome = pilotoNome
)
}
}
Ao tocar em um Switch ou no botão de salto temporal, disparamos context.tardisDataStore.edit:
// Gravação do Switch de Modo Neon
Switch(
checked = modoNeon,
onCheckedChange = { checked ->
coroutineScope.launch {
context.tardisDataStore.edit { preferences ->
preferences[MODO_NEON_KEY] = checked
}
}
}
)
// Gravação do Salto Espaço-Temporal
Button(
onClick = {
val novoDestino = listOf(
"Gallifrey (Constelação de Kasterborous)",
"Marte (Base Bowie One, 2059)",
"Skaro (Império Dalek)",
"Londres (Totter's Lane, 1963)"
).random()
coroutineScope.launch {
context.tardisDataStore.edit { preferences ->
preferences[COORDENADAS_KEY] = novoDestino
}
}
Toast.makeText(context, "VWOORP! Saltando para: $novoDestino", Toast.LENGTH_SHORT).show()
}
) {
Text("MATERIALIZAR TARDIS NO VÓRTICE")
}
| Sintoma Observado | Causa Provável | Como Resolver |
|---|---|---|
IllegalStateException: There are multiple DataStores active for the same file |
O delegado preferencesDataStore foi criado dentro de um Composable ou Activity repetidamente. |
Declare val Context.dataStore by preferencesDataStore(name = "...") como extensão global de nível superior no arquivo Kotlin (fora de qualquer classe). |
| As preferências são salvas, mas a tela não atualiza | A leitura do DataStore não foi convertida em estado Compose. | Utilize context.dataStore.data.map { ... }.collectAsState(initial = ...) para criar um estado reativo. |
Calling edit must be done from a coroutine |
O método edit { ... } é uma suspend function e foi chamado fora de um escopo assíncrono. |
Envolva a chamada em coroutineScope.launch { context.dataStore.edit { ... } }. |
| Ao fechar e reabrir o app, os dados são perdidos | O nome do arquivo no preferencesDataStore(name = "...") foi modificado entre execuções. |
Mantenha o nome da chave constante (ex: "tardis_settings"). |
Set<String> com as últimas 5 coordenadas visitadas usando stringSetPreferencesKey("historico_destinos").LaunchedEffect que resfria os motores da TARDIS após cada salto.OutlinedTextField) para o jogador alterar e salvar o nome do seu Doutor(a) no DataStore.Disponível em: app/src/main/java/br/com/curso/tardis/MainActivity.kt.
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd proj_aplicacoes_full_stack/projetos/android_p13_tardis_services
./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta
proj_aplicacoes_full_stack/projetos/android_p13_tardis_services.