Skip to main content

Overview

TecMeli uses Hilt, Google’s recommended dependency injection library for Android, built on top of Dagger. Hilt provides a standardized way to inject dependencies throughout the app, reducing boilerplate and improving testability.

Why Dependency Injection?

Testability

Dependencies can be easily mocked or replaced in tests

Loose Coupling

Classes don’t create their dependencies, making code more flexible

Reusability

Dependencies are configured once and reused across the app

Lifecycle Management

Hilt manages object lifecycles (Singleton, ViewModel-scoped, etc.)

Hilt Setup

Application Class

The entry point for Hilt is the Application class annotated with @HiltAndroidApp.
// TecMeliApp.kt
package com.alcalist.tecmeli

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp  // Triggers Hilt code generation
class TecMeliApp : Application()
@HiltAndroidApp must be added to your Application class. This annotation triggers Hilt’s code generation and allows Hilt to provide dependencies throughout the app.

Activity Injection

// MainActivity.kt
package com.alcalist.tecmeli

import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint  // Enables injection in this Activity
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TECMELITheme {
                NavigationWrapper()  // Navigation contains screens with ViewModels
            }
        }
    }
}
@AndroidEntryPoint makes Hilt-injected ViewModels available to Composables in this Activity.

Hilt Modules

Hilt modules define how to provide dependencies. TecMeli has two main modules in core/di/:

1. NetworkModule

Location: core/di/NetworkModule.kt Purpose: Provides network-related dependencies (OkHttp, Retrofit, API interfaces).
package com.alcalist.tecmeli.core.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module  // Marks this as a Hilt module
@InstallIn(SingletonComponent::class)  // Lives as long as the app
object NetworkModule {

    private const val BASE_URL = "https://api.mercadolibre.com/"

    /**
     * Provides API configuration from BuildConfig
     */
    @Provides
    @Singleton
    fun provideApiConfig(): ApiConfig {
        return ApiConfig(
            clientId = BuildConfig.CLIENT_ID,
            clientSecret = BuildConfig.CLIENT_SECRET,
            refreshToken = BuildConfig.REFRESH_TOKEN
        )
    }

    /**
     * Provides HTTP logging interceptor for debugging
     */
    @Provides
    @Singleton
    fun provideLoggingInterceptor(): HttpLoggingInterceptor {
        return HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }
    }

    /**
     * Provides configured OkHttpClient with auth and logging
     */
    @Provides
    @Singleton
    fun provideOkHttpClient(
        authInterceptor: AuthInterceptor,
        tokenAuthenticator: TokenAuthenticator,
        loggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(authInterceptor)  // Adds Authorization header
            .authenticator(tokenAuthenticator)  // Refreshes token on 401
            .addInterceptor(loggingInterceptor)  // Logs requests/responses
            .build()
    }

    /**
     * Provides main Retrofit instance
     */
    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    /**
     * Provides Mercado Libre API interface
     */
    @Provides
    @Singleton
    fun provideMeliApi(retrofit: Retrofit): MeliApi {
        return retrofit.create(MeliApi::class.java)
    }

    /**
     * Provides Auth API with separate client (no auth interceptor)
     * to avoid infinite loops during token refresh
     */
    @Provides
    @Singleton
    fun provideAuthApi(loggingInterceptor: HttpLoggingInterceptor): AuthApi {
        val authClient = OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()

        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(authClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(AuthApi::class.java)
    }
}
Key concepts:
Tells Hilt this object provides dependencies.
Dependencies live for the entire app lifecycle. Other options:
  • ActivityComponent: Lives as long as the Activity
  • ViewModelComponent: Lives as long as the ViewModel
  • FragmentComponent: Lives as long as the Fragment
Marks a function that provides a dependency. Hilt will call this function when the dependency is needed.
Ensures only one instance exists app-wide. The same instance is reused.
fun provideOkHttpClient(
    authInterceptor: AuthInterceptor,  // Hilt provides this
    tokenAuthenticator: TokenAuthenticator,  // Hilt provides this
    loggingInterceptor: HttpLoggingInterceptor  // Hilt provides this
): OkHttpClient
Hilt automatically resolves dependencies. If provideOkHttpClient needs AuthInterceptor, Hilt will:
  1. Look for a @Provides function that returns AuthInterceptor, OR
  2. Look for an @Inject constructor in AuthInterceptor

2. RepositoryModule

Location: core/di/RepositoryModule.kt Purpose: Binds repository interfaces to their implementations.
package com.alcalist.tecmeli.core.di

import com.alcalist.tecmeli.data.repository.ProductRepositoryImpl
import com.alcalist.tecmeli.data.repository.TokenRepositoryImpl
import com.alcalist.tecmeli.domain.repository.ProductRepository
import com.alcalist.tecmeli.domain.repository.TokenRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

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

    /**
     * Binds TokenRepository interface to TokenRepositoryImpl
     */
    @Binds
    @Singleton
    abstract fun bindTokenRepository(
        tokenRepositoryImpl: TokenRepositoryImpl
    ): TokenRepository

    /**
     * Binds ProductRepository interface to ProductRepositoryImpl
     */
    @Binds
    @Singleton
    abstract fun bindProductRepository(
        productRepositoryImpl: ProductRepositoryImpl
    ): ProductRepository
}
Key differences from NetworkModule:
RepositoryModule is an abstract class because it uses @Binds, which requires abstract functions.
@Binds is used when:
  • The implementation has an @Inject constructor
  • You’re just telling Hilt “use this implementation for this interface”
  • More efficient than @Provides (less generated code)
@Provides is used when:
  • You need to configure the object (builder pattern, parameters)
  • The class doesn’t have an @Inject constructor (third-party libraries)
// @Binds: Just map interface to implementation
@Binds
abstract fun bindProductRepository(
    impl: ProductRepositoryImpl
): ProductRepository

// @Provides: Need to configure the object
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .baseUrl(BASE_URL)  // Configuration needed
        .client(okHttpClient)
        .build()
}

Constructor Injection

Classes can request dependencies via their constructors using @Inject.

Repository Example

// data/repository/ProductRepositoryImpl.kt
class ProductRepositoryImpl @Inject constructor(
    private val meliApi: MeliApi  // Hilt provides this from NetworkModule
) : ProductRepository {
    
    override suspend fun searchProducts(query: String): Result<List<Product>> = safeApiCall(
        call = { meliApi.searchProducts(query = query) },
        transform = { body -> body.results.map { it.toDomain() } }
    )
}
How it works:
  1. RepositoryModule says “use ProductRepositoryImpl for ProductRepository
  2. ProductRepositoryImpl has @Inject constructor
  3. Constructor needs MeliApi
  4. Hilt looks for MeliApi provider → finds provideMeliApi() in NetworkModule
  5. provideMeliApi() needs Retrofit
  6. Hilt looks for Retrofit provider → finds provideRetrofit()
  7. Chain continues until all dependencies are resolved
  8. Hilt creates the object graph and injects everything

Use Case Example

// domain/usecase/GetProductsUseCase.kt
class GetProductsUseCase @Inject constructor(
    private val repository: ProductRepository  // Interface, not implementation
) {
    suspend operator fun invoke(query: String): Result<List<Product>> {
        return if (query.isBlank()) {
            Result.success(emptyList())
        } else {
            repository.searchProducts(query)
        }
    }
}
Use cases depend on interfaces (ProductRepository), not implementations. Hilt provides the implementation bound in RepositoryModule.

ViewModel Example

// ui/screen/home/HomeViewModel.kt
@HiltViewModel  // Special annotation for ViewModels
class HomeViewModel @Inject constructor(
    private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
    // ...
}
  • Tells Hilt this is a ViewModel
  • Enables ViewModel injection in Composables via hiltViewModel()
  • ViewModels are scoped to Navigation destinations
  • Survive configuration changes automatically

Interceptor Example

// core/network/AuthInterceptor.kt
class AuthInterceptor @Inject constructor(
    private val tokenRepository: TokenRepository
) : Interceptor {
    
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        if (originalRequest.url.encodedPath.contains("oauth/token")) {
            return chain.proceed(originalRequest)
        }

        val token = tokenRepository.getAccessToken()
        val requestBuilder = originalRequest.newBuilder()
        
        token?.let {
            requestBuilder.addHeader("Authorization", "Bearer $it")
        }
        
        return chain.proceed(requestBuilder.build())
    }
}

Dependency Graph

Here’s how dependencies flow in TecMeli: Hilt automatically resolves this entire graph!

Scopes

Hilt provides different scopes for different lifecycles.
ScopeComponentLifetimeUse Case
@SingletonSingletonComponentApp lifetimeNetwork client, database, repositories
@ActivityScopedActivityComponentActivity lifetimeActivity-specific services
@ViewModelScopedViewModelComponentViewModel lifetimeViewModel-specific dependencies
@ActivityRetainedScopedActivityRetainedComponentActivity (survives config changes)Shared ViewModels
In TecMeli:
  • Network components: @Singleton (used throughout the app)
  • Repositories: @Singleton (shared across ViewModels)
  • ViewModels: Automatically scoped to Navigation destinations

Injection in Composables

ViewModel Injection

@Composable
fun HomeScreen(
    navigateToDetail: (String) -> Unit,
    viewModel: HomeViewModel = hiltViewModel()  // Inject ViewModel
) {
    val uiState by viewModel.uiState.collectAsState()
    // ...
}
hiltViewModel() function:
  • Provided by androidx.hilt:hilt-navigation-compose
  • Injects the ViewModel with all dependencies
  • Scopes ViewModel to the Navigation destination
  • Handles lifecycle automatically

Manual Injection (Advanced)

For non-ViewModel dependencies in Composables:
@Composable
fun MyScreen(
    customDependency: MyDependency = EntryPointAccessors.fromActivity(
        LocalContext.current as ComponentActivity,
        MyEntryPoint::class.java
    ).getMyDependency()
) {
    // ...
}

@EntryPoint
@InstallIn(ActivityComponent::class)
interface MyEntryPoint {
    fun getMyDependency(): MyDependency
}
This is rarely needed. Prefer passing dependencies through ViewModels.

Testing with Hilt

Hilt makes testing easier by allowing you to replace dependencies.

ViewModel Test (Without Hilt)

class HomeViewModelTest {
    
    private lateinit var viewModel: HomeViewModel
    private val mockGetProductsUseCase = mockk<GetProductsUseCase>()
    
    @Before
    fun setup() {
        // Manual injection - no Hilt needed
        viewModel = HomeViewModel(mockGetProductsUseCase)
    }
    
    @Test
    fun `searchProducts emits success state`() = runTest {
        coEvery { mockGetProductsUseCase("laptop") } returns Result.success(listOf(mockProduct))
        
        viewModel.searchProducts("laptop")
        
        assertEquals(UiState.Success(listOf(mockProduct)), viewModel.uiState.value)
    }
}

Repository Test (Without Hilt)

class ProductRepositoryImplTest {
    
    private lateinit var repository: ProductRepositoryImpl
    private val mockApi = mockk<MeliApi>()
    
    @Before
    fun setup() {
        repository = ProductRepositoryImpl(mockApi)
    }
    
    @Test
    fun `searchProducts maps DTOs to domain models`() = runTest {
        val mockResponse = SearchResponseDto(results = listOf(mockResultDto))
        coEvery { mockApi.searchProducts(any()) } returns Response.success(mockResponse)
        
        val result = repository.searchProducts("laptop")
        
        assertTrue(result.isSuccess)
    }
}
Key point: Because we use constructor injection, tests can create objects manually without Hilt.

Integration Tests with Hilt

For tests that need the full dependency graph:
@HiltAndroidTest
class AppIntegrationTest {
    
    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var repository: ProductRepository
    
    @Before
    fun setup() {
        hiltRule.inject()
    }
    
    @Test
    fun testWithRealDependencies() {
        // repository is injected by Hilt with real implementations
    }
}

Common Patterns

Providing Multiple Implementations

Use qualifiers when you need multiple instances of the same type:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainRetrofit

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    
    @Provides
    @Singleton
    @MainRetrofit
    fun provideMainRetrofit(okHttpClient: OkHttpClient): Retrofit { ... }
    
    @Provides
    @Singleton
    @AuthRetrofit
    fun provideAuthRetrofit(okHttpClient: OkHttpClient): Retrofit { ... }
}

// Usage
class MyRepository @Inject constructor(
    @MainRetrofit private val mainRetrofit: Retrofit
) { ... }

Optional Dependencies

class MyClass @Inject constructor(
    @Named("optional") private val optionalDep: String?  // Nullable
) { ... }

@Provides
@Named("optional")
fun provideOptional(): String? = null

Best Practices

Use @Inject constructor over field injection. Constructor injection:
  • Makes dependencies explicit
  • Enables manual instantiation in tests
  • Ensures all dependencies are provided
// Good
class MyClass @Inject constructor(
    private val dependency: Dependency
)

// Avoid
class MyClass {
    @Inject lateinit var dependency: Dependency
}
@Binds is more efficient than @Provides for simple interface bindings.
// Efficient
@Binds
abstract fun bindRepository(impl: RepositoryImpl): Repository

// Less efficient (but sometimes necessary)
@Provides
fun provideRepository(impl: RepositoryImpl): Repository = impl
Use the narrowest scope possible:
  • @Singleton: Only for truly app-wide dependencies
  • @ViewModelScoped: For ViewModel-specific dependencies
  • No scope: New instance every time (stateless utilities)
Separate modules by concern:
  • NetworkModule: Network dependencies
  • RepositoryModule: Repository bindings
  • DatabaseModule: Database dependencies (if added)
Inject interfaces, not implementations.
// Good
class UseCase @Inject constructor(
    private val repository: ProductRepository  // Interface
)

// Avoid
class UseCase @Inject constructor(
    private val repository: ProductRepositoryImpl  // Concrete class
)

Troubleshooting

Error: Hilt components are not generatedSolution: Add @HiltAndroidApp to your Application class.
Error: ViewModel not found or ViewModelProvider.Factory not setSolution: Add @AndroidEntryPoint to your Activity/Fragment.
Error: Dependency cycle detectedSolution:
  • Restructure dependencies to break the cycle
  • Use Provider<T> or Lazy<T> to delay initialization
class A @Inject constructor(
    private val bProvider: Provider<B>  // Lazy initialization
)
Error: MyInterface cannot be provided without an @Provides or @Binds annotated methodSolution: Add a binding in a module:
@Binds
abstract fun bindMyInterface(impl: MyImplementation): MyInterface

Summary

Hilt in TecMeli

  • Setup: @HiltAndroidApp on Application, @AndroidEntryPoint on Activity
  • Modules: NetworkModule (network deps), RepositoryModule (repository bindings)
  • Injection: @Inject constructor for classes, @Provides for configuration
  • Scopes: @Singleton for app-wide, @HiltViewModel for ViewModels
  • Testing: Constructor injection enables easy mocking
  • Benefits: Automatic dependency resolution, lifecycle management, testability

Clean Architecture

See how DI supports Clean Architecture principles

MVVM Pattern

Learn how ViewModels receive dependencies via Hilt

Build docs developers (and LLMs) love