Injecao de dependencia (DI) e um padrao fundamental no desenvolvimento de software que promove baixo acoplamento, alta testabilidade e codigo mais facil de manter. Em vez de uma classe criar suas proprias dependencias, elas sao fornecidas externamente, permitindo trocar implementacoes sem alterar o codigo consumidor. No ecossistema Kotlin, duas solucoes se destacam: Koin, um framework leve baseado em DSL Kotlin, e Hilt, a solucao oficial do Google baseada no Dagger. Neste guia, exploraremos ambas as abordagens com exemplos praticos, comparacoes e cenarios de uso recomendados.
Por Que Usar Injecao de Dependencia
Sem DI, classes criam suas dependencias 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 implementacao real sem alterar PedidoService.
Koin: Injecao de Dependencia com DSL Kotlin
O Koin e um framework de DI leve que usa DSL Kotlin em vez de geracao de codigo ou anotacoes. E popular por sua simplicidade e integracao natural com Kotlin.
Configuracao
// 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 unica instancia (singleton), enquanto factory cria uma nova instancia a cada injecao. 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 Injecao no Codigo
// 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: Injecao de Dependencia Oficial do Google
O Hilt e construido sobre o Dagger e oferece integracao profunda com componentes Android. Usa geracao de codigo em tempo de compilacao, resultando em melhor performance em runtime.
Configuracao
// 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 Injecao 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, configuracao rapida e suporte a Kotlin Multiplatform. Sua DSL e intuitiva e nao requer geracao de codigo. Porem, erros de configuracao so sao detectados em runtime.
O Hilt oferece verificacao em tempo de compilacao, melhor performance em aplicacoes grandes e integracao profunda com o ecossistema Android. No entanto, exige mais configuracao 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 Praticas para Injecao de Dependencia
- Injete interfaces, nao 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 sao mais faceis de manter.
- Evite Service Locator quando possivel: embora Koin permita
by inject()em qualquer lugar, prefira constructor injection na maioria dos casos. - Defina escopos corretamente: singletons consomem memoria durante toda a vida da aplicacao. Use
factoryou escopos mais restritos quando a instancia nao precisa ser compartilhada. - Teste a configuracao de DI: o Koin oferece
checkModules()para verificar se todos os modulos estao configurados corretamente.
Erros Comuns e Armadilhas
- Dependencias 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 concorrencia.
- Esquecer anotacoes no Hilt: cada Activity, Fragment e ViewModel que usa injecao precisa das anotacoes corretas (
@AndroidEntryPoint,@HiltViewModel). - Modulos Koin nao registrados: adicionar um modulo e esquecer de inclui-lo no
startKoinresulta em erros de runtime. - Over-engineering: para projetos muito simples, DI manual (passando dependencias pelo construtor) pode ser suficiente sem framework nenhum.
Conclusao e Proximos Passos
A injecao de dependencia 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 alem, explore como DI se integra com Clean Architecture, consulte nossos guias sobre testes para aprender a usar mocks com DI e estude modulos avancados como Work Manager injection e Navigation injection.