Introdução à Injeção de Dependência em Kotlin
No desenvolvimento moderno com Kotlin, especialmente na criação de aplicativos Android, o princípio Dependency Injection (DI) tornou-se não apenas uma boa prática, mas uma necessidade. A injeção de dependências permite escrever código limpo, testável e escalável, reduzindo o acoplamento entre os componentes do sistema.
Neste artigo, vamos entender o que é Dependency Injection, quais problemas ele resolve e como implementar DI em Kotlin — desde a abordagem manual até o uso de frameworks populares como Hilt e Koin.
O que é Dependency Injection (Injeção de Dependência)?
Dependency Injection é um padrão de projeto no qual um objeto recebe suas dependências externamente, em vez de criá-las por conta própria. Em vez de escrever val engine = Engine() dentro da classe, nós "injetamos" uma instância pronta através do construtor, método ou campo.
Vamos considerar um exemplo simples sem DI:
class Car { private val engine = Engine() // Acoplamento rígido
fun start() { engine.start() }}Problema: a classe Car está rigidamente acoplada a uma implementação específica de Engine. É difícil de testar e estender.
Agora, um exemplo com DI via construtor:
class Car(private val engine: Engine) { // Dependência fornecida externamente
fun start() { engine.start() }}
// Usoval engine = Engine()val car = Car(engine)car.start()Agora Car não sabe como Engine é criado. Podemos facilmente substituí-lo por um MockEngine para testes ou por outra implementação.
Por que DI é importante em Kotlin?
Kotlin, como uma linguagem moderna, oferece excelentes recursos para DI:
- Null Safety — Kotlin ajuda a evitar NullPointerException ao trabalhar com dependências.
- Sintaxe concisa — construtores com parâmetros tornam o DI natural.
- Compatibilidade com Java — todos os frameworks DI populares (Dagger, Hilt) suportam Kotlin.
- Estilo funcional — Kotlin permite usar lambdas e delegates para implementar DI.
Formas de implementar DI em Kotlin
1. Injeção Manual (Manual DI)
A maneira mais simples é passar dependências através do construtor. Para projetos pequenos, isso geralmente é suficiente.
class Database(private val connection: Connection)
class UserRepository(private val database: Database)
class UserViewModel(private val userRepository: UserRepository)
// Montagem manual de dependênciasfun main() { val connection = Connection("postgres://localhost:5432") val database = Database(connection) val repository = UserRepository(database) val viewModel = UserViewModel(repository)}Desvantagem: à medida que o projeto cresce, o gerenciamento manual se torna complicado.
2. Service Locator (Antipadrão ou solução temporária)
Às vezes, usa-se o Service Locator — um objeto central que armazena todas as dependências. No entanto, isso é considerado um antipadrão, pois esconde as dependências reais da classe.
object ServiceLocator { private val database = Database() private val repository = UserRepository(database) fun provideViewModel() = UserViewModel(repository)}
// Usoval viewModel = ServiceLocator.provideViewModel()3. Frameworks DI: Hilt e Koin
Para projetos sérios, é melhor usar soluções prontas. Vamos considerar os dois mais populares para Kotlin/Android.
Hilt (baseado em Dagger)
Hilt é a biblioteca DI oficial do Google para Android. É baseada no Dagger, mas com uma API simplificada.
// 1. Anotação @HiltAndroidApp na Application@HiltAndroidAppclass MyApplication : Application(