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

🦇 P12: Batcomputer (Injeção de Dependências com Hilt & Sensores)

Bem-vindo à Fase 7: Arquitetura Avançada & Hardware! 🦇

Neste projeto de alto nível profissional, você implementará o padrão ouro de engenharia de software no ecossistema Android: a Injeção de Dependências (Dependency Injection - DI) utilizando o framework oficial recomendado pelo Google: o Dagger Hilt, integrado aos sensores físicos do smartphone (Acelerômetro & Sensor de Movimento) através do SensorManager.

Desenvolveremos o Batcomputer v2.0, uma central de inteligência tática com tema cyberpunk e dark Gotham que monitora a telemetria do Bat-Traje em tempo real, detecta impactos de batalha via hardware e lista os crimes ativos em Gotham com filtros dinâmicos de ameaça — tudo de forma 100% desacoplada e modular!

Fluxo Completo de Telas do Batcomputer


📱 Galeria de Telas da Aplicação

1. HUD Tático & Telemetria 2. Alerta de Impacto / Furtivo 3. Filtros Dinâmicos de Ameaça
Tela 1 - HUD Tela 2 - Impacto Tela 3 - Filtros
Painel tático escuro com sensores X/Y/Z e crimes. Gatilho de aceleração súbita no acelerômetro. Filtragem reativa por nível de perigo em Gotham.

✅ Pré-requisitos e Continuidade

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


🎯 Objetivos de Aprendizagem

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

  1. Configurar o Dagger Hilt no Android Studio: Declarar o plugin KAPT, configurar o container global @HiltAndroidApp e declarar pontos de entrada com @AndroidEntryPoint.
  2. Injetar Repositórios em ViewModels: Anotar construtores com @Inject e classes de controle com @HiltViewModel.
  3. Criar Módulos Hilt de Provisão: Utilizar anotações @Module, @InstallIn(SingletonComponent::class), @Provides e @Singleton para entregar instâncias únicas com escopo de aplicativo.
  4. Acessar Hardware Nativo com SensorManager: Coletar dados do acelerômetro (Sensor.TYPE_ACCELEROMETER), emitir fluxos reativos com StateFlow e disparar o modo de alerta/impacto.
  5. Conectar ViewModel e Compose de Forma Limpa: Obter instâncias injetadas diretamente na UI via hiltViewModel().
  6. Implementar Filtros Táticos Reativos: Utilizar FilterChip e listas filtradas declarativamente no Jetpack Compose.

🏗️ Arquitetura e Injeção de Dependências (Dagger Hilt)

graph TD
    A[BatcomputerApplication: @HiltAndroidApp] --> B[Container de Injeção Global Hilt]
    
    B --> C[AppModule: @Module @InstallIn SingletonComponent]
    C -->|@Provides @Singleton| D[CrimeRepositoryImpl: CrimeRepository]
    C -->|@Provides @Singleton| E[BatSensorRepositoryImpl: BatSensorRepository]
    
    E --> F[SensorManager do Android: TYPE_ACCELEROMETER]
    F -->|onSensorChanged| G[StateFlow: BatSensorData X/Y/Z]
    
    D --> H[BatViewModel: @HiltViewModel @Inject]
    G --> H
    
    H --> I[MainActivity: @AndroidEntryPoint]
    I --> J[BatcomputerScreen: viewModel = hiltViewModel]
    
    J --> K[Card de Telemetria: Acelerômetro em Tempo Real]
    J --> L[Chips de Filtro: TODOS / MÁXIMO / ALTO / MÉDIO]
    J --> M[LazyColumn: Lista de Crimes em Gotham]

📖 Dicionário Técnico do Projeto

Termo / Anotação O que é e para que serve?
Injeção de Dependências (DI) Padrão arquitetural onde objetos recebem suas dependências prontas através do construtor, eliminando o acoplamento forte (new Objeto()) e facilitando testes automatizados.
Dagger Hilt Biblioteca opinativa e oficial do Google construída sobre o Dagger 2 que automatiza a injeção em todo o ciclo de vida do Android.
@HiltAndroidApp Anotação obrigatória na classe Application que gera o código-fonte do contêiner raiz de injeção de dependências do projeto.
@AndroidEntryPoint Anotação colocada na Activity ou Fragment que habilita o Hilt a injetar objetos e fornecer ViewModels compatíveis.
@HiltViewModel Anotação que avisa ao Hilt para criar a Factory automática do ViewModel e gerenciar seu ciclo de vida.
@Module & @Provides Declaradores de métodos que ensinam ao Hilt como criar e fornecer instâncias de interfaces ou bibliotecas de terceiros.
@Singleton Garante que o Hilt crie uma única instância do objeto para toda a vida útil da aplicação na memória RAM.
SensorManager Serviço de sistema do Android (Context.SENSOR_SERVICE) responsável por listar e monitorar os sensores de hardware (acelerômetro, giroscópio, proximidade, luz).

🛠️ 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: Batcomputer (ou android_p12_batcomputer_sensores)
    • Package name: br.com.curso.batcomputer
    • 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: Configurando os Arquivos Gradle para Dagger Hilt

  1. No arquivo raiz build.gradle (Project: ...):
    buildscript {
     dependencies {
         classpath 'com.google.dagger:hilt-android-gradle-plugin:2.50'
     }
    }
    
  2. No arquivo app/build.gradle: ```groovy plugins { id ‘com.android.application’ id ‘org.jetbrains.kotlin.android’ id ‘org.jetbrains.kotlin.kapt’ id ‘com.google.dagger.hilt.android’ }

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’

// Dagger Hilt (Injeção de Dependências)
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' } ``` *Clique em **Sync Now**.*

🏗️ Passo 3: Criando a Classe Application (BatcomputerApplication.kt)

Crie o arquivo app/src/main/java/br/com/curso/batcomputer/BatcomputerApplication.kt:

package br.com.curso.batcomputer

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class BatcomputerApplication : Application()

📄 Passo 4: Registrando no Manifesto (AndroidManifest.xml)

Abra app/src/main/AndroidManifest.xml e adicione o atributo android:name=".BatcomputerApplication" dentro de <application>:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".BatcomputerApplication"
        android:allowBackup="true"
        android:label="Batcomputer"
        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 5: Modelos de Dados (CrimeReport & BatSensorData)

Abra app/src/main/java/br/com/curso/batcomputer/MainActivity.kt e declare os modelos:

data class CrimeReport(
    val id: Int,
    val title: String,
    val villain: String,
    val dangerLevel: String,
    val location: String,
    val details: String
)

data class BatSensorData(
    val accelX: Float = 0f,
    val accelY: Float = 0f,
    val accelZ: Float = 9.8f,
    val isStealthActive: Boolean = false,
    val sensorStatus: String = "Monitorando Movimento e Impacto"
)

🔌 Passo 6: Interfaces e Contratos de Repositório

interface CrimeRepository {
    fun getActiveCrimes(): List<CrimeReport>
}

interface BatSensorRepository {
    val sensorFlow: StateFlow<BatSensorData>
    fun startMonitoring()
    fun stopMonitoring()
    fun triggerSimulatedShake()
}

⚙️ Passo 7: Implementação dos Repositórios com Sensor de Hardware

A classe BatSensorRepositoryImpl escuta o acelerômetro do hardware e converte os eventos físicos em um fluxo reativo de dados:

class BatSensorRepositoryImpl @Inject constructor(
    @ApplicationContext private val context: Context
) : BatSensorRepository, SensorEventListener {

    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    private val _sensorFlow = MutableStateFlow(BatSensorData())
    override val sensorFlow: StateFlow<BatSensorData> = _sensorFlow.asStateFlow()

    override fun startMonitoring() {
        accelerometer?.let {
            sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
        }
    }

    override fun stopMonitoring() {
        sensorManager.unregisterListener(this)
    }

    override fun triggerSimulatedShake() {
        val current = _sensorFlow.value
        _sensorFlow.value = current.copy(
            accelX = (5..15).random().toFloat(),
            accelY = (5..15).random().toFloat(),
            accelZ = (12..25).random().toFloat(),
            isStealthActive = !current.isStealthActive,
            sensorStatus = if (!current.isStealthActive) "MODO FURTIVO ATIVADO! (Impacto Detectado)" else "Modo de Patrulha Normal"
        )
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if (event != null && event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
            val x = event.values[0]
            val y = event.values[1]
            val z = event.values[2]

            val acceleration = Math.sqrt((x * x + y * y + z * z).toDouble()).toFloat()
            val isImpact = acceleration > 15f

            _sensorFlow.value = BatSensorData(
                accelX = x,
                accelY = y,
                accelZ = z,
                isStealthActive = isImpact,
                sensorStatus = if (isImpact) "ALERTA: Impacto Alto Detectado!" else "Sensores Operacionais em Gotham"
            )
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}

🧩 Passo 8: Módulo de Provisão do Hilt (AppModule)

Configura a fábrica do Hilt para entregar instâncias únicas (@Singleton) dos repositórios:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideCrimeRepository(): CrimeRepository {
        return CrimeRepositoryImpl()
    }

    @Provides
    @Singleton
    fun provideBatSensorRepository(@ApplicationContext context: Context): BatSensorRepository {
        return BatSensorRepositoryImpl(context)
    }
}

🧠 Passo 9: ViewModel Injetado (BatViewModel)

O ViewModel recebe ambos os repositórios prontos via construtor com @Inject:

@HiltViewModel
class BatViewModel @Inject constructor(
    private val crimeRepository: CrimeRepository,
    private val sensorRepository: BatSensorRepository
) : ViewModel() {

    val crimes = crimeRepository.getActiveCrimes()
    val sensorData = sensorRepository.sensorFlow
    var selectedFilter by mutableStateOf("TODOS")

    init {
        sensorRepository.startMonitoring()
    }

    fun simulateShake() {
        sensorRepository.triggerSimulatedShake()
    }

    override fun onCleared() {
        super.onCleared()
        sensorRepository.stopMonitoring()
    }
}

🖥️ Passo 10: Interface Tática no Compose (BatcomputerScreen)

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BatcomputerTheme {
                BatcomputerScreen()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BatcomputerScreen(viewModel: BatViewModel = hiltViewModel()) {
    val sensorState by viewModel.sensorData.collectAsState()
    val filteredCrimes = remember(viewModel.selectedFilter) {
        if (viewModel.selectedFilter == "TODOS") viewModel.crimes
        else viewModel.crimes.filter { it.dangerLevel == viewModel.selectedFilter }
    }

    Scaffold(
        containerColor = MaterialTheme.colorScheme.background,
        topBar = {
            CenterAlignedTopAppBar(
                title = { Text("🦇 BATCOMPUTER v2.0", fontWeight = FontWeight.ExtraBold, letterSpacing = 2.sp) },
                colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF16161A), titleContentColor = Color(0xFFFFEB3B))
            )
        }
    ) { padding ->
        LazyColumn(modifier = Modifier.padding(padding).fillMaxSize().padding(horizontal = 16.dp)) {
            item {
                Spacer(modifier = Modifier.height(10.dp))

                // Card de Telemetria com Acelerômetro
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(containerColor = if (sensorState.isStealthActive) Color(0xFF2E1015) else Color(0xFF1A1A22)),
                    shape = RoundedCornerShape(16.dp)
                ) {
                    Column(modifier = Modifier.padding(16.dp)) {
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("📡 TELEMETRIA DO BAT-TRAJE", fontWeight = FontWeight.Black, fontSize = 12.sp, color = if (sensorState.isStealthActive) Color(0xFFFF1744) else Color(0xFFFFEB3B))
                            Text(if (sensorState.isStealthActive) "MODO FURTIVO" else "HILT ATIVO", fontWeight = FontWeight.Bold, fontSize = 10.sp, color = if (sensorState.isStealthActive) Color(0xFFFF1744) else Color(0xFF00E676))
                        }
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(sensorState.sensorStatus, color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
                        Spacer(modifier = Modifier.height(8.dp))
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("Acel X: ${String.format("%.1f", sensorState.accelX)}", color = Color.Gray, fontSize = 11.sp) Text("Acel Y:${String.format("%.1f", sensorState.accelY)}", color = Color.Gray, fontSize = 11.sp)
                            Text("Acel Z: ${String.format("%.1f", sensorState.accelZ)}", color = Color.Gray, fontSize = 11.sp)
                        }
                        Spacer(modifier = Modifier.height(12.dp))
                        OutlinedButton(onClick = { viewModel.simulateShake() }, modifier = Modifier.fillMaxWidth().height(40.dp), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFFFEB3B))) {
                            Text("⚡ SIMULAR AGITAMENTO / SENSOR", fontSize = 11.sp, fontWeight = FontWeight.Bold)
                        }
                    }
                }

                Spacer(modifier = Modifier.height(18.dp))

                // Chips de Filtro
                Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    listOf("TODOS", "MÁXIMO", "ALTO", "MÉDIO").forEach { filter ->
                        FilterChip(
                            selected = viewModel.selectedFilter == filter,
                            onClick = { viewModel.selectedFilter = filter },
                            label = { Text(filter, fontSize = 11.sp, fontWeight = FontWeight.Bold) },
                            colors = FilterChipDefaults.filterChipColors(selectedContainerColor = Color(0xFFFFEB3B), selectedLabelColor = Color.Black)
                        )
                    }
                }

                Spacer(modifier = Modifier.height(10.dp))
                Text("CRIMES ATIVOS EM GOTHAM (${filteredCrimes.size})", fontSize = 11.sp, fontWeight = FontWeight.Black, color = Color.Gray)
                Spacer(modifier = Modifier.height(6.dp))
            }

            items(filteredCrimes, key = { it.id }) { crime ->
                Card(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), colors = CardDefaults.cardColors(containerColor = Color(0xFF16161A)), shape = RoundedCornerShape(14.dp)) {
                    Column(modifier = Modifier.padding(16.dp)) {
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text(crime.title, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 15.sp)
                            Text(crime.dangerLevel, color = if (crime.dangerLevel == "MÁXIMO") Color(0xFFFF1744) else Color(0xFFFFEB3B), fontWeight = FontWeight.Black, fontSize = 11.sp)
                        }
                        Spacer(modifier = Modifier.height(4.dp))
                        Text("Suspeito: ${crime.villain}", color = Color(0xFF00E5FF), fontSize = 12.sp, fontWeight = FontWeight.SemiBold) Text("Local:${crime.location}", color = Color.Gray, fontSize = 12.sp)
                    }
                }
            }
        }
    }
}

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

Sintoma Observado Causa Provável Como Resolver
HiltInjector cannot find BatcomputerApplication A classe BatcomputerApplication não foi anotada com @HiltAndroidApp ou não foi declarada na tag <application> do AndroidManifest.xml. Verifique se a anotação @HiltAndroidApp existe e se android:name=".BatcomputerApplication" está presente no manifesto.
Cannot create an instance of class BatViewModel A MainActivity não foi anotada com @AndroidEntryPoint. Adicione @AndroidEntryPoint logo acima da declaração class MainActivity : ComponentActivity().
[Dagger/MissingBinding] CrimeRepository cannot be provided without an @Provides-annotated method O módulo Hilt não possui um método anotado com @Provides para a interface CrimeRepository. Verifique se AppModule contém @Provides @Singleton fun provideCrimeRepository(): CrimeRepository.
No Emulador, o acelerômetro não se move O emulador não envia eventos de movimento físico espontaneamente. Clique no botão ⚡ SIMULAR AGITAMENTO / SENSOR na tela do app ou abra o painel Extended Controls > Virtual Sensors do emulador para rotacionar o aparelho virtual.

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

  1. 🦇 Bat-Sinal com Sensor de Luz: Injete o sensor de luminosidade (Sensor.TYPE_LIGHT) e projete o Bat-Sinal na tela sempre que o ambiente estiver escuro (abaixo de 10 lux)!
  2. 🗺️ Mapeamento de Coordenadas de Gotham: Crie uma rota de navegação para visualizar o ponto do crime no mapa estilizado em modo escuro.
  3. 💾 Persistência de Alertas: Armazene os relatórios atendidos pelo Batman em um banco de dados local com Room Database.

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

Disponível em: app/src/main/java/br/com/curso/batcomputer/MainActivity.kt e app/src/main/java/br/com/curso/batcomputer/BatcomputerApplication.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_p12_batcomputer_sensores

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