Overview
The TecMeli app uses Dagger Hilt for dependency injection. The core DI configuration is split into two main modules:
NetworkModule : Provides HTTP clients, Retrofit instances, and API services
RepositoryModule : Binds repository interfaces to their implementations
All dependencies are scoped as @Singleton to optimize resource usage across the application.
NetworkModule
Hilt module responsible for providing all network-related dependencies including HTTP clients, security interceptors, authenticators, and Retrofit API instances.
Implementation
package com.alcalist.tecmeli.core.di
import com.alcalist.tecmeli.BuildConfig
import com.alcalist.tecmeli.core.network.ApiConfig
import com.alcalist.tecmeli.core.network.AuthInterceptor
import com.alcalist.tecmeli.core.network.TokenAuthenticator
import com.alcalist.tecmeli.data.remote.api.AuthApi
import com.alcalist.tecmeli.data.remote.api.MeliApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn (SingletonComponent:: class )
object NetworkModule {
private const val BASE_URL = "https://api.mercadolibre.com/"
@Provides
@Singleton
fun provideApiConfig (): ApiConfig {
return ApiConfig (
clientId = BuildConfig.CLIENT_ID,
clientSecret = BuildConfig.CLIENT_SECRET,
refreshToken = BuildConfig.REFRESH_TOKEN
)
}
@Provides
@Singleton
fun provideLoggingInterceptor (): HttpLoggingInterceptor {
return HttpLoggingInterceptor (). apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
@Provides
@Singleton
fun provideOkHttpClient (
authInterceptor: AuthInterceptor ,
tokenAuthenticator: TokenAuthenticator ,
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient {
return OkHttpClient. Builder ()
. addInterceptor (authInterceptor)
. authenticator (tokenAuthenticator)
. addInterceptor (loggingInterceptor)
. build ()
}
@Provides
@Singleton
fun provideRetrofit (okHttpClient: OkHttpClient ): Retrofit {
return Retrofit. Builder ()
. baseUrl (BASE_URL)
. client (okHttpClient)
. addConverterFactory (GsonConverterFactory. create ())
. build ()
}
@Provides
@Singleton
fun provideMeliApi (retrofit: Retrofit ): MeliApi {
return retrofit. create (MeliApi:: class .java)
}
@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)
}
}
Provided Dependencies
provideApiConfig()
Provides base API configuration by extracting values from BuildConfig.
Contains client credentials and refresh token for OAuth authentication
@Provides
@Singleton
fun provideApiConfig (): ApiConfig
Configuration values are loaded from BuildConfig which is populated from gradle.properties or environment variables.
provideLoggingInterceptor()
Configures an interceptor to log HTTP requests and responses to the console.
Configured with BODY level logging for complete request/response visibility
@Provides
@Singleton
fun provideLoggingInterceptor (): HttpLoggingInterceptor
BODY level logging should only be used in debug builds as it exposes sensitive data. Consider using conditional configuration based on build type.
provideOkHttpClient()
Configures the main OkHttp client with security and logging interceptors.
Adds access token to request headers
tokenAuthenticator
TokenAuthenticator
required
Handles automatic token refresh on 401 responses
loggingInterceptor
HttpLoggingInterceptor
required
Logs network traffic for debugging
Fully configured HTTP client with authentication and logging
@Provides
@Singleton
fun provideOkHttpClient (
authInterceptor: AuthInterceptor ,
tokenAuthenticator: TokenAuthenticator ,
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient
Configuration order:
AuthInterceptor - Adds authorization header
TokenAuthenticator - Refreshes tokens on 401
HttpLoggingInterceptor - Logs requests/responses
provideRetrofit()
Provides the base Retrofit instance configured with Gson converter.
The configured OkHttp client with interceptors
Base Retrofit instance for creating API services
@Provides
@Singleton
fun provideRetrofit (okHttpClient: OkHttpClient ): Retrofit
Configuration:
Base URL: https://api.mercadolibre.com/
Converter: Gson (for JSON serialization/deserialization)
HTTP Client: Custom OkHttpClient with auth and logging
provideMeliApi()
Creates the implementation of the Mercado Libre products API.
The configured Retrofit instance
Retrofit service interface for product-related endpoints
@Provides
@Singleton
fun provideMeliApi (retrofit: Retrofit ): MeliApi
provideAuthApi()
Creates a specialized Retrofit instance for authentication processes.
loggingInterceptor
HttpLoggingInterceptor
required
Logging interceptor for debugging auth requests
Retrofit service interface for OAuth token operations
@Provides
@Singleton
fun provideAuthApi (loggingInterceptor: HttpLoggingInterceptor ): AuthApi
Critical: This uses a simplified OkHttp client without AuthInterceptor to avoid infinite loops during token refresh. Never add AuthInterceptor to the auth client!
Why a separate client?
Prevents circular dependency during token refresh
The auth endpoint doesn’t require Bearer token
Avoids infinite loop when refreshing expired tokens
RepositoryModule
Hilt module responsible for binding repository interfaces to their concrete implementations using @Binds.
Implementation
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
@Singleton
abstract fun bindTokenRepository (
tokenRepositoryImpl: TokenRepositoryImpl
): TokenRepository
@Binds
@Singleton
abstract fun bindProductRepository (
productRepositoryImpl: ProductRepositoryImpl
): ProductRepository
}
Bound Repositories
bindTokenRepository()
Binds the TokenRepository interface to its implementation.
tokenRepositoryImpl
TokenRepositoryImpl
required
Concrete implementation of token management
Interface for token operations (get, refresh, clear)
@Binds
@Singleton
abstract fun bindTokenRepository (
tokenRepositoryImpl: TokenRepositoryImpl
): TokenRepository
bindProductRepository()
Binds the ProductRepository interface to its implementation.
productRepositoryImpl
ProductRepositoryImpl
required
Concrete implementation of product data operations
Interface for product-related data operations
@Binds
@Singleton
abstract fun bindProductRepository (
productRepositoryImpl: ProductRepositoryImpl
): ProductRepository
Usage Examples
Injecting Dependencies in ViewModels
@HiltViewModel
class ProductSearchViewModel @Inject constructor (
private val productRepository: ProductRepository
) : ViewModel () {
fun searchProducts (query: String ) {
viewModelScope. launch {
val result = productRepository. searchProducts (query)
// Handle result
}
}
}
Injecting Dependencies in Repositories
class ProductRepositoryImpl @Inject constructor (
private val meliApi: MeliApi ,
private val safeApiCallExecutor: SafeApiCallExecutor ,
private val productMapper: ProductMapper
) : ProductRepository {
override suspend fun searchProducts (query: String ): Result < List < Product >> {
return safeApiCallExecutor. execute (
call = { meliApi. searchProducts (query) },
transform = { response ->
response.results. map { productMapper. toDomain (it) }
}
)
}
}
Injecting Dependencies in Activities
@AndroidEntryPoint
class MainActivity : ComponentActivity () {
@Inject
lateinit var tokenRepository: TokenRepository
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
// tokenRepository is automatically injected
}
}
Architecture Diagram
┌─────────────────────────────────────────────┐
│ NetworkModule │
├─────────────────────────────────────────────┤
│ │
│ ApiConfig ──> BuildConfig │
│ │
│ HttpLoggingInterceptor │
│ │ │
│ ├──> OkHttpClient (Main) │
│ │ ├─ AuthInterceptor │
│ │ ├─ TokenAuthenticator │
│ │ └─ HttpLoggingInterceptor │
│ │ │
│ └──> OkHttpClient (Auth) │
│ └─ HttpLoggingInterceptor │
│ │
│ Retrofit (Main) ──> MeliApi │
│ Retrofit (Auth) ──> AuthApi │
│ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ RepositoryModule │
├─────────────────────────────────────────────┤
│ │
│ TokenRepositoryImpl ──> TokenRepository │
│ ProductRepositoryImpl ──> ProductRepository│
│ │
└─────────────────────────────────────────────┘
Best Practices
Use @Binds for interface binding
Prefer @Binds over @Provides when binding interfaces to implementations. It generates less code and is more efficient.
Keep auth client separate
Always maintain a separate OkHttp client for authentication without AuthInterceptor to prevent circular dependencies.
Singleton scope for stateless services
Network services and repositories should be singletons as they’re stateless and reusable across the app.
Extract sensitive config to BuildConfig
Order interceptors correctly
Add AuthInterceptor before LoggingInterceptor so that logs show the final request with auth headers.
Testing Considerations
Providing Test Doubles
For unit tests, you can provide mock implementations:
@Module
@TestInstallIn (
components = [SingletonComponent:: class ],
replaces = [NetworkModule:: class ]
)
object FakeNetworkModule {
@Provides
@Singleton
fun provideMeliApi (): MeliApi = mockk ()
@Provides
@Singleton
fun provideAuthApi (): AuthApi = mockk ()
}
Integration Tests
For integration tests, you can use a real OkHttp client with MockWebServer:
@Module
@TestInstallIn (
components = [SingletonComponent:: class ],
replaces = [NetworkModule:: class ]
)
object TestNetworkModule {
@Provides
@Singleton
fun provideRetrofit (mockWebServer: MockWebServer ): Retrofit {
return Retrofit. Builder ()
. baseUrl (mockWebServer. url ( "/" ))
. addConverterFactory (GsonConverterFactory. create ())
. build ()
}
}
See Also