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 factory ou 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 startKoin resulta 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.