📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

🌌 P13: TARDIS Panel (Jetpack DataStore Preferences & Reatividade)

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!

Fluxo Completo de Telas do TARDIS Panel


📱 Galeria de Telas da Aplicação

1. Painel Clássico Azul 2. Modo Neon Quântico 3. Salto Espaço-Temporal
Tela 1 - Painel Azul Tela 2 - Modo Neon Tela 3 - Salto 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.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Configurar o Jetpack DataStore Preferences: Declarar a dependência oficial e criar o delegado global preferencesDataStore(name = "tardis_settings").
  2. Definir Chaves Tipadas com Segurança: Utilizar booleanPreferencesKey() e stringPreferencesKey() para evitar erros de digitação e corrupção de tipos.
  3. Escutar Mudanças em Tempo Real com Kotlin Flows: Consumir dataStore.data.map { ... } e convertê-lo em estados observáveis do Compose com collectAsState().
  4. Executar Gravações Assíncronas com Corrotinas: Utilizar context.dataStore.edit { ... } dentro de coroutineScope.launch sem travar a Thread principal da UI (Main Thread).
  5. Aplicar Transições de Tema Reativas: Usar animateColorAsState para alternar suavemente entre a paleta clássica e o modo neon fluorescente da TARDIS.
  6. Implementar Boas Práticas de Segurança em Armazenamento Local: Garantir o isolamento de preferências no diretório protegido da Sandbox do Android.

🏗️ Arquitetura e Ciclo Reativo do DataStore

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]

🛡️ Boas Práticas de Segurança: DataStore vs SharedPreferences

[!IMPORTANT] Por que o Google substituiu o SharedPreferences pelo DataStore Preferences?

  1. Segurança contra Travamentos (ANR - Application Not Responding): O SharedPreferences antigo 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.
  2. Isolamento na Sandbox: O DataStore grava o arquivo tardis_settings.preferences_pb no diretório privado da aplicação (/data/data/br.com.curso.tardis/files/datastore/), inacessível para outros aplicativos instalados no celular.
  3. Segurança de Tipos (Type-Safety): Não há risco de ClassCastException em tempo de execução, pois as chaves são explicitamente tipadas via booleanPreferencesKey ou stringPreferencesKey.
  4. Higiene de Versionamento: O arquivo .gitignore foi configurado para garantir que arquivos temporários e chaves locais nunca sejam commitados.

📖 Dicionário Técnico do Projeto

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.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: TARDIS Panel (ou android_p13_tardis_services)
    • Package name: br.com.curso.tardis
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde o Gradle sincronizar.

📦 Passo 2: Adicionando a Dependência do DataStore no Gradle (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.


📄 Passo 3: Manifesto do Aplicativo (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>

🔑 Passo 4: Inicialização Global e Chaves Tipadas do DataStore

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")

🎨 Passo 5: Tema Reativo com Animação Neon (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
    )
}

🔄 Passo 6: Coleta Reativa de Preferências na Raiz do App

@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
        )
    }
}

🎛️ Passo 7: Painel da TARDIS e Gravação com Coroutines

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")
}

🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

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").

🏆 Desafios e Upgrades (Mão na Massa!)

  1. 📜 Histórico de Saltos no DataStore: Salve uma Set<String> com as últimas 5 coordenadas visitadas usando stringSetPreferencesKey("historico_destinos").
  2. ⏱️ Timer de Sobrecarga do Motor: Adicione um temporizador com LaunchedEffect que resfria os motores da TARDIS após cada salto.
  3. 👤 Edição de Perfil do Piloto: Crie um campo de texto (OutlinedTextField) para o jogador alterar e salvar o nome do seu Doutor(a) no DataStore.

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/tardis/MainActivity.kt.


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

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

2. Execute a aplicação e os testes

./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.