🌌 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:


🎯 Objetivo do Projeto

Construir um app de configurações de ficção científica com as seguintes metas:

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


🛠️ Passo 1: Adicionando Dependências do DataStore (Gradle)

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

  1. Declare suas chaves do DataStore dentro do Compose:
    val SOM_VIAGEM = booleanPreferencesKey("som_viagem")
    val MODO_NEON = booleanPreferencesKey("modo_neon")
    
  2. 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)
    
  3. 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:


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