Injeção de dependência parece um assunto abstrato até o primeiro app Android crescer de verdade. No começo, instanciar um Repository dentro do ViewModel funciona. Depois entram API, banco local, analytics, autenticação, feature flags, cache, testes, ambiente de homologação e múltiplos módulos. Sem um grafo claro de dependências, cada tela começa a criar objetos do seu jeito e o projeto fica difícil de testar.
No ecossistema Android com Kotlin, Hilt é o caminho oficial mais comum para resolver esse problema. Ele simplifica o Dagger, integra com Application, Activity, Fragment, ViewModel, WorkManager e testes, e valida boa parte do grafo em tempo de compilação. Em 2026, dominar Hilt continua sendo um diferencial forte para vagas Android, principalmente quando combinado com Jetpack Compose, MVVM com Kotlin, Room, Ktor Client resiliente e testes Android com Compose e Maestro.
Este guia mostra como pensar Hilt em um app real: onde configurar, quando criar módulos, como escolher scopes, como usar qualifiers, como injetar ViewModels e como substituir dependências em testes sem transformar tudo em boilerplate.
O que Hilt resolve no projeto Android?
Hilt cria e entrega objetos para as classes que precisam deles. Em vez de uma tela construir manualmente ApiClient, UsuarioRepository, Database, Logger e Analytics, você declara como cada dependência nasce e deixa o framework montar o grafo.
Isso resolve problemas práticos:
- evita duplicação de configuração em várias telas;
- centraliza ciclo de vida de objetos caros, como banco e clientes HTTP;
- facilita trocar implementação real por fake em testes;
- deixa dependências explícitas no construtor;
- reduz singletons globais improvisados;
- antecipa erros de configuração durante o build.
A ideia não é “usar framework porque é moderno”. A ideia é manter a arquitetura legível quando o app passa de três telas para trinta.
Configuração básica no Gradle
Em um projeto Android com Gradle Kotlin DSL, você normalmente adiciona o plugin do Hilt no projeto e no módulo do app. As versões devem acompanhar o Android Gradle Plugin e a documentação atual, mas a estrutura é esta:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.52")
ksp("com.google.dagger:hilt-android-compiler:2.52")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
ksp("androidx.hilt:hilt-compiler:1.2.0")
}
Muitos projetos antigos usam kapt. Em bases novas, vale preferir KSP quando a combinação de versões do projeto permite. Se você está organizando versões em libs.versions.toml, veja também o guia de Gradle Version Catalog em Kotlin.
No Application, marque a classe com @HiltAndroidApp:
@HiltAndroidApp
class AppKotlinBrasil : Application()
Esse ponto inicial permite que Hilt gere os componentes necessários para o app inteiro.
Injeção por construtor é o padrão preferido
Sempre que possível, injete dependências pelo construtor. Isso deixa claro o que a classe precisa para funcionar e facilita testes unitários sem Android.
class UsuarioRepository @Inject constructor(
private val api: UsuarioApi,
private val dao: UsuarioDao,
private val logger: AppLogger,
) {
suspend fun buscarPerfil(id: String): Usuario {
logger.debug("Buscando perfil $id")
return dao.buscar(id) ?: api.buscarUsuario(id).also { dao.salvar(it) }
}
}
Repare que o repository não sabe como UsuarioApi, UsuarioDao ou AppLogger são criados. Ele apenas declara suas necessidades. Essa separação combina bem com Clean Architecture: casos de uso e repositories dependem de contratos, enquanto detalhes de rede, banco e analytics ficam nas bordas.
ViewModel com Hilt e Compose
Para ViewModels, use @HiltViewModel e injete dependências pelo construtor:
@HiltViewModel
class PerfilViewModel @Inject constructor(
private val buscarPerfil: BuscarPerfilUseCase,
) : ViewModel() {
private val _estado = MutableStateFlow(PerfilState())
val estado: StateFlow<PerfilState> = _estado.asStateFlow()
fun carregar(id: String) {
viewModelScope.launch {
_estado.value = PerfilState(carregando = true)
runCatching { buscarPerfil(id) }
.onSuccess { _estado.value = PerfilState(usuario = it) }
.onFailure { _estado.value = PerfilState(erro = "Não foi possível carregar") }
}
}
}
Em uma tela Compose com Navigation, recupere o ViewModel com hiltViewModel():
@Composable
fun PerfilRoute(
viewModel: PerfilViewModel = hiltViewModel(),
) {
val estado by viewModel.estado.collectAsStateWithLifecycle()
PerfilScreen(estado = estado, onRetry = { viewModel.carregar("me") })
}
Evite injetar dependências diretamente em Composables. A tela deve receber estado e callbacks. O ViewModel conversa com os casos de uso; os casos de uso conversam com repositories. Essa divisão mantém UI, regra de negócio e infraestrutura separadas.
Quando criar módulos Hilt
A injeção por construtor cobre muita coisa, mas alguns objetos precisam de módulo porque são criados por builder, vêm de biblioteca externa ou dependem de configuração manual. Exemplos: Retrofit, OkHttp, Room, DataStore, Firebase, Apollo, clientes Ktor e implementações de interfaces.
@Module
@InstallIn(SingletonComponent::class)
object RedeModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("https://api.exemplo.com/")
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
@Provides
fun provideUsuarioApi(retrofit: Retrofit): UsuarioApi =
retrofit.create(UsuarioApi::class.java)
}
Use módulos para construção, não para esconder arquitetura. Se todo objeto do app vira um método @Provides, provavelmente você está perdendo a vantagem da injeção por construtor.
Entendendo scopes sem exagerar
Scopes dizem por quanto tempo uma instância deve viver. O mais conhecido é @Singleton, que cria uma instância por aplicação. Ele faz sentido para banco, cliente HTTP, storage de preferências e serviços compartilhados.
Mas nem tudo precisa ser singleton. Um use case geralmente pode ser sem escopo. Um objeto leve e stateless pode ser recriado sem problema. Scopes demais deixam o grafo rígido e podem segurar referências mais tempo do que deveriam.
Alguns componentes comuns:
| Scope | Quando usar | Cuidado |
|---|---|---|
@Singleton | banco, client HTTP, repository com cache global | não guardar referência de Activity ou View |
@ActivityRetainedScoped | estado compartilhado entre ViewModels da mesma Activity | não confundir com ciclo de tela Compose |
@ViewModelScoped | dependência específica daquele ViewModel | não usar para objeto que precisa sobreviver ao app |
| sem scope | use cases simples, mappers, formatadores | ok para objetos baratos |
A regra prática: comece sem scope. Adicione scope quando houver motivo claro de ciclo de vida, custo de criação ou compartilhamento de estado.
Interfaces, implementações e @Binds
Quando uma camada depende de interface, use @Binds para dizer qual implementação concreta Hilt deve entregar.
interface AuthRepository {
suspend fun usuarioAtual(): Usuario?
}
class AuthRepositoryRemoto @Inject constructor(
private val api: AuthApi,
private val tokenStore: TokenStore,
) : AuthRepository {
override suspend fun usuarioAtual(): Usuario? = api.me(tokenStore.token())
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindAuthRepository(
impl: AuthRepositoryRemoto,
): AuthRepository
}
Prefira @Binds para interface-implementação simples. Use @Provides quando precisar executar lógica de criação.
Qualifiers para dependências do mesmo tipo
Às vezes existem duas dependências do mesmo tipo: duas URLs base, dois dispatchers, dois clientes HTTP ou duas instâncias de analytics. Hilt não consegue adivinhar qual usar. Para isso, use qualifiers.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiBaseUrl
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class CdnBaseUrl
@Module
@InstallIn(SingletonComponent::class)
object UrlModule {
@Provides
@ApiBaseUrl
fun provideApiBaseUrl(): String = "https://api.exemplo.com/"
@Provides
@CdnBaseUrl
fun provideCdnBaseUrl(): String = "https://cdn.exemplo.com/"
}
Na classe consumidora:
class ImagemService @Inject constructor(
@CdnBaseUrl private val cdnBaseUrl: String,
)
Não use @Named("api") em tudo por preguiça. Qualifiers próprios são mais refatoráveis, documentam intenção e evitam erro de string.
Testes com substituição de dependências
Um dos maiores ganhos de Hilt é trocar dependências reais por fakes em testes de integração Android. Para testes instrumentados, use @HiltAndroidTest e HiltAndroidRule:
@HiltAndroidTest
class PerfilScreenTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Before
fun setup() {
hiltRule.inject()
}
}
Para substituir um módulo inteiro em teste, use @TestInstallIn:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RedeModule::class]
)
object FakeRedeModule {
@Provides
@Singleton
fun provideUsuarioApi(): UsuarioApi = FakeUsuarioApi()
}
Isso permite testar tela, navegação e ViewModel contra dados previsíveis, sem bater na API real. Em testes unitários puros de ViewModel ou use case, você nem precisa subir Hilt: crie a classe manualmente com fakes no construtor. Hilt é ferramenta de composição do app, não obrigação para todo teste.
Hilt, WorkManager e tarefas em background
Para workers, Hilt também ajuda bastante. Em apps offline-first, é comum um Worker precisar de repository, logger e sincronizador. Use @HiltWorker e @AssistedInject:
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val syncRepository: SyncRepository,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result =
runCatching { syncRepository.sincronizarPendentes() }
.fold(
onSuccess = { Result.success() },
onFailure = { Result.retry() },
)
}
Combine isso com o guia de WorkManager com Kotlin para não misturar fila offline, regra de negócio e agendamento em uma classe gigante.
Erros comuns com Hilt
Os problemas mais frequentes em projetos Android não vêm do Hilt em si, mas de uso apressado:
- Injetar contexto errado: use
@ApplicationContextpara dependências de app. Evite guardarActivityem singletons. - Transformar tudo em singleton: singletons demais criam estado global disfarçado.
- Colocar regra de negócio em módulos: módulos devem construir objetos, não decidir fluxo de produto.
- Injetar em Composables diretamente: prefira ViewModel e parâmetros de tela.
- Usar Service Locator manual junto com Hilt: misturar padrões aumenta confusão.
- Não testar o grafo: pelo menos uma suíte instrumentada deve garantir que o app sobe com dependências reais.
- Qualifiers genéricos:
@Named("x")espalhado vira armadilha em refactors.
Se o build falha com erro de binding ausente, leia a mensagem de baixo para cima: Hilt geralmente mostra qual classe pediu a dependência, qual tipo faltou e em qual componente.
Hilt ou Koin?
Koin continua sendo uma opção Kotlin-first, leve e muito usada, especialmente em projetos multiplataforma. Hilt costuma ser melhor quando o app é Android nativo, segue recomendações oficiais do Google e quer validação mais forte em tempo de compilação. A comparação detalhada está em Koin vs Dagger/Hilt.
Para um app Android profissional em 2026, escolher Hilt é uma decisão conservadora e forte. A documentação, exemplos oficiais, integração com Jetpack e reconhecimento em entrevistas pesam bastante. Para KMP com compartilhamento amplo, Koin ou injeção manual por construtor podem ser mais naturais.
Checklist antes de levar para produção
Antes de considerar o setup de Hilt pronto, revise:
@HiltAndroidAppconfigurado noApplication;- dependências principais usando injeção por construtor;
- módulos apenas para objetos que precisam de construção especial;
@Singletonlimitado a objetos realmente compartilhados;- qualifiers próprios para valores do mesmo tipo;
- ViewModels com
@HiltViewModele estado exposto porStateFlow; - testes com fakes para API, banco ou storage sensível;
- nenhum singleton guardando referência de
Activity,FragmentouView; - CI rodando build que valide geração de código.
Hilt não substitui arquitetura. Ele só torna a composição da arquitetura mais segura. O ganho aparece quando cada classe declara claramente o que precisa, quando ciclos de vida são escolhidos com intenção e quando testes conseguem trocar bordas reais por fakes sem gambiarra.
Para quem quer crescer como dev Android Kotlin, Hilt é um desses assuntos que conectam código, arquitetura e empregabilidade. Ele aparece em projetos reais porque resolve um problema real: manter um app grande testável, modular e previsível. Domine o básico, evite scopes desnecessários, escreva módulos pequenos e use testes para provar que o grafo funciona.
Também vale olhar para outros ecossistemas de backend e mobile: em aplicações server-side, frameworks de DI e configuração existem há anos em Java, Spring e .NET; em stacks mais explícitas, como Go para APIs, a composição manual é mais comum. Entender esses contrastes ajuda a usar Hilt pelo motivo certo: reduzir acoplamento sem esconder demais o funcionamento do app.