🦇 P12: Batcomputer (Injeção de Dependências com Hilt)
Bem-vindo à Fase 7: Arquitetura Avançada & Navegação! 🦇
Neste projeto de alto impacto, você aprenderá a implementar o padrão arquitetural de Injeção de Dependências (DI) utilizando o framework oficial recomendado pelo Google: o Hilt (baseado no poderoso Dagger). Criaremos o aplicativo de inteligência tática Batcomputer, acoplando repositórios de relatórios de crimes em Gotham diretamente em nossos ViewModels sem instanciá-los manualmente, atingindo os padrões profissionais de engenharia de software do mercado.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P10: Arena Social
🎯 Objetivo do Projeto
Estruturar um app robusto e altamente desacoplado contendo:
- Camada de Dados Abstrata: Declarar contratos de serviços e repositórios através de interfaces limpas do Kotlin.
- Controle de Ciclo de Vida do Hilt: Usar anotações para injetar instâncias únicas (Singletons) e automáticas em ViewModels.
- Provisão de Módulos: Configurar o container de dependências global do Hilt.
- Painel Tático Dark: Renderizar os crimes em andamento sob alerta máximo de perigo na interface moderna do Jetpack Compose.
graph TD
A[Início / Inicializar Batcomputer] --> B[Hilt Container detecta classes anotadas]
B --> C[Instancia o CrimeRepositoryImpl de forma Singleton]
C --> D[Injeta a instância no BatViewModel via construtor]
D --> E[BatcomputerScreen recolhe crimes do ViewModel]
E --> F[Renderizar relatórios de perigo no Compose]
📖 Dicionário do Projeto
- Injeção de Dependências (DI): Padrão de projeto usado para passar (injetar) objetos que uma classe precisa para funcionar ao invés de deixar a própria classe criar esses objetos internamente. Isso facilita a substituição de classes de rede ou bancos por dados simulados (mocks) em testes.
- Dagger Hilt: A biblioteca oficial e opinativa de injeção de dependências do Google construída sobre o Dagger 2. Simplifica drasticamente a configuração de injeção em aplicativos Android.
- @Inject constructor(): Anotação colocada no construtor de uma classe que avisa ao Hilt que ele está autorizado a criar e gerenciar instâncias dessa classe automaticamente.
- @Module & @Provides: Usados quando precisamos injetar objetos complexos ou criados por terceiros (como o Retrofit ou o Room), ensinando ao Hilt as instruções exatas de criação.
- @Singleton: Garante que o Hilt crie apenas uma única instância desse objeto para todo o aplicativo, economizando memória.
🛠️ Passo 1: Configurando as Dependências do Hilt (Gradle)
- No arquivo
build.gradle (Project :...)da raiz, configure o plugin de classpath do Hilt:buildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48' } } - No arquivo
build.gradle (Module :app), aplique o plugin do Hilt e adicione as dependências no bloco: ```gradle plugins { … id ‘kotlin-kapt’ id ‘dagger.hilt.android.plugin’ }
dependencies { // Hilt Dependency Injection implementation “com.google.dagger:hilt-android:2.48” kapt “com.google.dagger:hilt-compiler:2.48” implementation “androidx.hilt:hilt-navigation-compose:1.1.0” … }
---
## 🛠️ Passo 2: Criando a Classe Application (`BatcomputerApp.kt`)
Toda injeção com Hilt necessita de um container pai de inicialização global do app.
1. Crie a classe `BatcomputerApp.kt` no seu pacote principal:
```kotlin
package br.com.curso.batcomputer
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class BatcomputerApp : Application()
- Não se esqueça de registrar esta classe no seu
AndroidManifest.xmldentro da tag<application>: ```xml
<application android:name=”.BatcomputerApp” … >
---
## 🧠 Passo 3: Declarando a Camada de Dados e Módulo Hilt
Criamos a interface de dados `CrimeRepository` e configuramos o módulo Hilt `AppModule` anotado com `@Module` e `@InstallIn(SingletonComponent::class)`. Esse módulo se responsabiliza por ensinar ao Hilt que, sempre que um ViewModel pedir a interface `CrimeRepository`, o Hilt deve criar e entregar a classe concreta `CrimeRepositoryImpl`.
---
## 🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
* **Filtro por Nível de Perigo**: adicione um campo de busca ou `Chip`s de filtro para exibir apenas os `crimes` cujo `dangerLevel` corresponda à seleção.
* **Novo Repositório Injetado**: crie uma interface `SuspectRepository` com sua implementação e um `@Provides` análogo no `AppModule`, exibindo uma segunda lista de suspeitos na tela.
---
# 📖 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/batcomputer/MainActivity.kt`
```kotlin
package br.com.curso.batcomputer
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BatcomputerTheme {
BatcomputerScreen()
}
}
}
}
@Composable
fun BatcomputerTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFFFEB3B), // Amarelo Batman
background = Color(0xFF09090A),
surface = Color(0xFF16161A)
),
content = content
)
}
// 1. MODELO DE DADOS
data class CrimeReport(val id: Int, val title: String, val dangerLevel: String, val location: String)
// 2. INTERFACE DO REPOSITÓRIO
interface CrimeRepository {
fun getActiveCrimes(): List<CrimeReport>
}
// 3. IMPLEMENTAÇÃO INJETADA PELO HILT
class CrimeRepositoryImpl @Inject constructor() : CrimeRepository {
override fun getActiveCrimes(): List<CrimeReport> {
return listOf(
CrimeReport(1, "Invasão no Asilo Arkham", "MÁXIMO", "Norte de Gotham"),
CrimeReport(2, "Roubo ao Banco de Gotham", "ALTO", "Centro Comercial"),
CrimeReport(3, "Sinal do Charada no Metrô", "MÉDIO", "Subsolo Leste")
)
}
}
// 4. MÓDULO DE PROVISÃO DO HILT
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideCrimeRepository(): CrimeRepository {
return CrimeRepositoryImpl()
}
}
// 5. VIEWMODEL INJETADO
class BatViewModel(private val repository: CrimeRepository) : ViewModel() {
val crimes = repository.getActiveCrimes()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BatcomputerScreen() {
val mockRepository = CrimeRepositoryImpl()
val viewModel = BatViewModel(mockRepository)
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
CenterAlignedTopAppBar(
title = { Text("BATCOMPUTER v2.0", fontWeight = FontWeight.ExtraBold, letterSpacing = 3.sp) },
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color(0xFF16161A),
titleContentColor = Color(0xFFFFEB3B)
)
)
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding).fillMaxSize().padding(16.dp)) {
items(viewModel.crimes) { crime ->
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(crime.title, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 16.sp)
Text(
text = crime.dangerLevel,
color = if (crime.dangerLevel == "MÁXIMO") Color.Red else Color(0xFFFFEB3B),
fontWeight = FontWeight.ExtraBold,
fontSize = 12.sp
)
}
Spacer(modifier = Modifier.height(8.dp))
Text("Localização: ${crime.location}", color = Color.Gray, fontSize = 14.sp)
}
}
}
}
}
}