Capítulo 12: Injeção de Dependências no Android: Koin vs Hilt e Arquitetura Modular

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual de Grafos de Injeção de Dependências (DI)

flowchart TD
    APP["Classe Application (Ponto de Partida do Grafo de DI)"] --> DI_CONTAINER["Contêiner de Injeção (Koin startKoin / Hilt @HiltAndroidApp)"]

    subgraph MODULES["Módulos de Dependência (Definição de Provedores)"]
        NET_MOD["Módulo de Rede: singleOf(::KtorHttpClient)"]
        DATA_MOD["Módulo de Dados: singleOf(::RoomAppDatabase)"]
        REPO_MOD["Módulo de Repositório: singleOf(::UsuarioRepositoryImpl) bind UsuarioRepository::class"]
        VM_MOD["Módulo de ViewModel: viewModelOf(::UsuarioViewModel)"]
    end

    DI_CONTAINER --> NET_MOD
    DI_CONTAINER --> DATA_MOD
    DATA_MOD --> REPO_MOD
    NET_MOD --> REPO_MOD
    REPO_MOD --> VM_MOD

    subgraph COMPOSE["Injeção Reativa na Camada de Apresentação (Compose)"]
        SCREEN["TelaUsuarioScreen()"]
        INJECT["val vm: UsuarioViewModel = koinViewModel() / hiltViewModel()"]
        SCREEN --> INJECT
    end

    VM_MOD -->|"Instanciação Automática com Escopo de Lifecycle"| INJECT

    style APP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style DI_CONTAINER fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style REPO_MOD fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
    style VM_MOD fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style SCREEN fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Princípio da Inversão de Controle e a Arquitetura Mobile

À medida que um aplicativo Android cresce, instanciar clientes HTTP, bancos de dados locais e repositórios manualmente dentro de cada Activity ou ViewModel gera um acoplamento crítico. Classes tornam-se impossíveis de testar de forma isolada, pois dependências concretas ficam engessadas no código.

A Injeção de Dependências (DI) inverte esse fluxo: as classes declaram o que precisam em seus construtores e um contêiner externo fornece essas instâncias:

  1. Hilt (Google Dagger para Android): É a biblioteca oficial recomendada pelo Google para projetos de grande porte. O Hilt gera código Java/Kotlin em tempo de compilação através de processadores de anotações (KSP - Kotlin Symbol Processing). Sua grande vantagem é a validação estática: se uma dependência estiver ausente no grafo, o projeto falha ao compilar, evitando crashes surpresa em tempo de execução.
  2. Koin (The Pragmatic Kotlin DI): É um framework puramente escrito em Kotlin que opera através de um DSL declarativo e Service Locator funcional. Ele não gera código adicional, não usa reflexão pesada e tem tempo de compilação quase instantâneo. É a escolha preferida em projetos que priorizam builds rápidos, Kotlin Multiplatform (KMP) e simplicidade de configuração.
  3. Escopos no Jetpack Compose: Tanto o Koin quanto o Hilt fornecem extensões específicas para o Compose (koinViewModel(), hiltViewModel()). Essas funções garantem que a instância do ViewModel seja obtida do ViewModelStoreOwner mais próximo, mantendo o estado vivo durante rotações e descartando-o quando a tela sai do grafo de navegação.

📊 2. Comparativo Arquitetural: Koin vs Hilt (Dagger)

Dimensão Técnica Koin (Kotlin Pragmatic DI) Hilt (Google Dagger Android)
Resolução do Grafo Runtime (lazy, sob demanda na primeira chamada) Compile-time (validação estática completa via KSP)
Impacto no Tempo de Build Zero impacto (não gera código nem exige KSP) Moderado (geração de classes e fábricas de grafo)
Sintaxe & Curva de Aprendizado Baixíssima (DSL simples: single, factory, viewModel) Média-alta (anotações: @AndroidEntryPoint, @Inject, @Provides)
Kotlin Multiplatform (KMP) Suporte nativo completo (Android, iOS, Desktop) Exclusivo para o ecossistema Android nativo
Detecção de Dependências Ausentes Dispara InstanceCreationException em runtime Falha o build do Gradle com relatório detalhado

💻 3. Laboratório Prático: Configuração de Módulos e Injeção com Koin 3.5+

O código abaixo demonstra a configuração completa de injeção de dependências em um projeto Android utilizando a sintaxe idiomática moderna do Koin:

package br.com.portal.mobile.android.cap12

import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.context.startKoin
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module

// 1. Interfaces e Implementações de Domínio/Dados
interface DispositivoRepository {
    fun obterNomeAparelho(): String
}

class DispositivoRepositoryImpl : DispositivoRepository {
    override fun obterNomeAparelho(): String = "Android 14 ARM64 Snapdragon"
}

// 2. ViewModel com Injeção de Construtor
class SistemaViewModel(
    private val repository: DispositivoRepository
) : ViewModel() {
    private val _informacao = MutableStateFlow("Carregando...")
    val informacao = _informacao.asStateFlow()

    fun carregarInfo() {
        viewModelScope.launch {
            _informacao.value = "Hardware: ${repository.obterNomeAparelho()}"
        }
    }
}

// 3. Módulo Koin com Construtores Automáticos (singleOf e viewModelOf)
val appModuloKoin = module {
    // Registra Singleton da interface mapeada para a implementação concreta
    singleOf(::DispositivoRepositoryImpl) { bind<DispositivoRepository>() }

    // Registra ViewModel com ciclo de vida gerenciado pelo AndroidX
    viewModelOf(::SistemaViewModel)
}

// 4. Inicialização na Classe Application
class PortalMobileApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@PortalMobileApplication)
            modules(appModuloKoin)
        }
    }
}

⚠️ 4. Armadilhas Comuns em Injeção de Dependências

  1. Injetar Contexto de Activity em Singletons: Criar um singleton que armazena uma referência para a Activity do Android (androidActivityScope) causa vazamento catastrófico de memória. Sempre injete o androidContext() (Application Context) para singletons de rede, banco ou preferências.
  2. Esquecer de Vincular a Interface (bind): Ao declarar singleOf(::RepositorioImpl), se seu ViewModel depender da interface Repositorio, o Koin lançará uma exceção afirmando que a interface não foi encontrada no grafo. Lembre-se sempre de encadear { bind<Repositorio>() }.
  3. Módulos Gigantes Monolíticos: Evite concentrar todas as dependências do aplicativo em um único arquivo de módulo. Divida seus módulos por camada ou por feature (redeModule, bancoModule, autenticacaoModule), facilitando a manutenção e os testes unitários.