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
factoryou 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
startKoinresulta 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.