Injeção de dependência (DI) é um padrão fundamental no desenvolvimento de software que promove baixo acoplamento, alta testabilidade e código mais fácil de manter. Em vez de uma classe criar suas proprias dependências, elas são fornecidas externamente, permitindo trocar implementacoes sem alterar o código consumidor. No ecossistema Kotlin, duas solucoes se destacam: Koin, um framework leve baseado em DSL Kotlin, e Hilt, a solução oficial do Google baseada no Dagger. Neste guia, exploraremos ambas as abordagens com exemplos práticos, comparações e cenários de uso recomendados.

Por Que Usar Injeção de Dependencia

Sem DI, classes criam suas dependências internamente, criando acoplamento forte:

// Sem DI - Acoplado e dificil de testar
class PedidoService {
    private val repository = PedidoRepositoryImpl(
        ApiClient(), // Dependencia interna
        DatabaseHelper.getInstance() // Singleton global
    )

    fun criarPedido(request: PedidoRequest): Pedido {
        return repository.salvar(request.toPedido())
    }
}

// Com DI - Desacoplado e testavel
class PedidoService(
    private val repository: PedidoRepository // Interface injetada
) {
    fun criarPedido(request: PedidoRequest): Pedido {
        return repository.salvar(request.toPedido())
    }
}

Com DI, podemos facilmente substituir PedidoRepository por um mock em testes ou trocar a implementação real sem alterar PedidoService.

Koin: Injeção de Dependencia com DSL Kotlin

O Koin e um framework de DI leve que usa DSL Kotlin em vez de geracao de código ou anotações. E popular por sua simplicidade e integração natural com Kotlin.

Configuração

// build.gradle.kts
dependencies {
    // Koin Core
    implementation("io.insert-koin:koin-core:3.5.3")
    // Koin Android
    implementation("io.insert-koin:koin-android:3.5.3")
    // Koin Compose (opcional)
    implementation("io.insert-koin:koin-androidx-compose:3.5.3")
    // Koin Test
    testImplementation("io.insert-koin:koin-test-junit5:3.5.3")
}

Definindo Modulos

val networkModule = module {
    single {
        HttpClient {
            install(ContentNegotiation) {
                json(Json { ignoreUnknownKeys = true })
            }
        }
    }

    single<ProdutoApiService> { ProdutoApiServiceImpl(get()) }
}

val databaseModule = module {
    single {
        Room.databaseBuilder(
            androidContext(),
            AppDatabase::class.java,
            "meu_app_db"
        ).build()
    }

    single { get<AppDatabase>().produtoDao() }
    single { get<AppDatabase>().pedidoDao() }
}

val repositoryModule = module {
    single<ProdutoRepository> {
        ProdutoRepositoryImpl(
            apiService = get(),
            produtoDao = get()
        )
    }

    single<PedidoRepository> {
        PedidoRepositoryImpl(
            apiService = get(),
            pedidoDao = get()
        )
    }
}

val useCaseModule = module {
    factory { CriarPedidoUseCase(get(), get()) }
    factory { ListarProdutosUseCase(get()) }
    factory { CancelarPedidoUseCase(get()) }
}

val viewModelModule = module {
    viewModel { ProdutoViewModel(get()) }
    viewModel { PedidoViewModel(get(), get(), get()) }
    viewModel { (pedidoId: Long) ->
        DetalhePedidoViewModel(pedidoId, get())
    }
}

A diferenca entre single e factory e crucial: single cria uma única instancia (singleton), enquanto factory cria uma nova instancia a cada injeção. Use viewModel para ViewModels do Android.

Iniciando o Koin

class MeuApp : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger(Level.DEBUG)
            androidContext(this@MeuApp)
            modules(
                networkModule,
                databaseModule,
                repositoryModule,
                useCaseModule,
                viewModelModule
            )
        }
    }
}

Usando Injeção no Código

// Em Activities e Fragments
class ProdutoFragment : Fragment() {
    private val viewModel: ProdutoViewModel by viewModel()
    private val logger: Logger by inject()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.carregarProdutos()
    }
}

// Em Jetpack Compose
@Composable
fun TelaProdutos(
    viewModel: ProdutoViewModel = koinViewModel()
) {
    val estado by viewModel.uiState.collectAsStateWithLifecycle()
    // Renderizar UI
}

// Em classes comuns (nao Android)
class MeuServico : KoinComponent {
    private val repository: ProdutoRepository by inject()
}

Hilt: Injeção de Dependencia Oficial do Google

O Hilt e construido sobre o Dagger e oferece integração profunda com componentes Android. Usa geracao de código em tempo de compilação, resultando em melhor performance em runtime.

Configuração

// build.gradle.kts (projeto)
plugins {
    id("com.google.dagger.hilt.android") version "2.50" apply false
}

// build.gradle.kts (app)
plugins {
    id("com.google.dagger.hilt.android")
    kotlin("kapt")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.50")
    kapt("com.google.dagger:hilt-android-compiler:2.50")
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
    testImplementation("com.google.dagger:hilt-android-testing:2.50")
    kaptTest("com.google.dagger:hilt-android-compiler:2.50")
}

Definindo Modulos Hilt

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

    @Provides
    @Singleton
    fun provideHttpClient(): HttpClient {
        return HttpClient {
            install(ContentNegotiation) {
                json(Json { ignoreUnknownKeys = true })
            }
        }
    }

    @Provides
    @Singleton
    fun provideApiService(httpClient: HttpClient): ProdutoApiService {
        return ProdutoApiServiceImpl(httpClient)
    }
}

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

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "meu_app_db"
        ).build()
    }

    @Provides
    fun provideProdutoDao(database: AppDatabase): ProdutoDao {
        return database.produtoDao()
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindProdutoRepository(
        impl: ProdutoRepositoryImpl
    ): ProdutoRepository

    @Binds
    @Singleton
    abstract fun bindPedidoRepository(
        impl: PedidoRepositoryImpl
    ): PedidoRepository
}

Usando Injeção com Hilt

@HiltAndroidApp
class MeuApp : Application()

@AndroidEntryPoint
class ProdutoFragment : Fragment() {
    private val viewModel: ProdutoViewModel by viewModels()
}

@HiltViewModel
class ProdutoViewModel @Inject constructor(
    private val listarProdutos: ListarProdutosUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    init {
        carregarProdutos()
    }

    fun carregarProdutos() {
        viewModelScope.launch {
            listarProdutos()
                .onSuccess { _uiState.value = UiState.Success(it) }
                .onFailure { _uiState.value = UiState.Error(it.message ?: "") }
        }
    }
}

// Em Compose
@Composable
fun TelaProdutos(
    viewModel: ProdutoViewModel = hiltViewModel()
) {
    val estado by viewModel.uiState.collectAsStateWithLifecycle()
    // Renderizar UI
}

Koin vs Hilt: Quando Usar Cada Um

O Koin e ideal para projetos que valorizam simplicidade, configuração rápida e suporte a Kotlin Multiplatform. Sua DSL e intuitiva e não requer geracao de código. Porem, erros de configuração só são detectados em runtime.

O Hilt oferece verificação em tempo de compilação, melhor performance em aplicações grandes e integração profunda com o ecossistema Android. No entanto, exige mais configuração e o uso de kapt ou ksp, que pode aumentar o tempo de build.

Para projetos Android puros de grande escala, o Hilt e geralmente a melhor escolha. Para projetos menores, backend com Ktor ou Kotlin Multiplatform, o Koin tende a ser mais adequado.

Boas Práticas para Injeção de Dependencia

  • Injete interfaces, não implementacoes: isso permite trocar implementacoes facilmente, especialmente em testes.
  • Use constructor injection: prefira injetar pelo construtor em vez de field injection. E mais explicito e testavel.
  • Organize modulos por feature ou camada: modulos pequenos e focados são mais faceis de manter.
  • Evite Service Locator quando possível: embora Koin permita by inject() em qualquer lugar, prefira constructor injection na maioria dos casos.
  • Defina escopos corretamente: singletons consomem memória durante toda a vida da aplicação. Use factory ou escopos mais restritos quando a instancia não precisa ser compartilhada.
  • Teste a configuração de DI: o Koin oferece checkModules() para verificar se todos os modulos estao configurados corretamente.

Erros Comuns e Armadilhas

  • Dependências circulares: A depende de B e B depende de A. Refatore extraindo uma interface ou um terceiro componente.
  • Singleton quando deveria ser factory: usar singleton para objetos com estado mutavel pode causar bugs de concorrência.
  • Esquecer anotações no Hilt: cada Activity, Fragment é ViewModel que usa injeção precisa das anotações corretas (@AndroidEntryPoint, @HiltViewModel).
  • Modulos Koin não registrados: adicionar um modulo e esquecer de inclui-lo no startKoin resulta em erros de runtime.
  • Over-engineering: para projetos muito simples, DI manual (passando dependências pelo construtor) pode ser suficiente sem framework nenhum.

Conclusão e Próximos Passos

A injeção de dependência e um pilar da arquitetura de software moderna. Tanto Koin quanto Hilt oferecem solucoes robustas para Kotlin, cada uma com suas vantagens. Escolha a que melhor se adapta ao seu projeto e equipe. Para ir além, explore como DI se integra com Clean Architecture, consulte nossos guias sobre testes para aprender a usar mocks com DI e estude modulos avançados como Work Manager injection e Navigation injection.