📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
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!
| 1. HUD Tático & Telemetria | 2. Alerta de Impacto / Furtivo | 3. Filtros Dinâmicos de Ameaça |
|---|---|---|
| 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. |
Antes de iniciar este laboratório de arquitetura, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
@HiltAndroidApp e declarar pontos de entrada com @AndroidEntryPoint.@Inject e classes de controle com @HiltViewModel.@Module, @InstallIn(SingletonComponent::class), @Provides e @Singleton para entregar instâncias únicas com escopo de aplicativo.Sensor.TYPE_ACCELEROMETER), emitir fluxos reativos com StateFlow e disparar o modo de alerta/impacto.hiltViewModel().FilterChip e listas filtradas declarativamente no Jetpack Compose.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]
| 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). |
File > New > New Project).Batcomputer (ou android_p12_batcomputer_sensores)br.com.curso.batcomputerKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)build.gradle (Project: ...):
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.50'
}
}
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**.*
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()
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>
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"
)
interface CrimeRepository {
fun getActiveCrimes(): List<CrimeReport>
}
interface BatSensorRepository {
val sensorFlow: StateFlow<BatSensorData>
fun startMonitoring()
fun stopMonitoring()
fun triggerSimulatedShake()
}
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) {}
}
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)
}
}
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()
}
}
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)
}
}
}
}
}
}
| 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. |
Sensor.TYPE_LIGHT) e projete o Bat-Sinal na tela sempre que o ambiente estiver escuro (abaixo de 10 lux)!Disponível em: app/src/main/java/br/com/curso/batcomputer/MainActivity.kt e app/src/main/java/br/com/curso/batcomputer/BatcomputerApplication.kt.
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
./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.