Skip to main content

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.
ApiConfig
ApiConfig
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.
HttpLoggingInterceptor
HttpLoggingInterceptor
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.
authInterceptor
AuthInterceptor
required
Adds access token to request headers
tokenAuthenticator
TokenAuthenticator
required
Handles automatic token refresh on 401 responses
loggingInterceptor
HttpLoggingInterceptor
required
Logs network traffic for debugging
OkHttpClient
OkHttpClient
Fully configured HTTP client with authentication and logging
@Provides
@Singleton
fun provideOkHttpClient(
    authInterceptor: AuthInterceptor,
    tokenAuthenticator: TokenAuthenticator,
    loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient
Configuration order:
  1. AuthInterceptor - Adds authorization header
  2. TokenAuthenticator - Refreshes tokens on 401
  3. HttpLoggingInterceptor - Logs requests/responses

provideRetrofit()

Provides the base Retrofit instance configured with Gson converter.
okHttpClient
OkHttpClient
required
The configured OkHttp client with interceptors
Retrofit
Retrofit
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.
retrofit
Retrofit
required
The configured Retrofit instance
MeliApi
MeliApi
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
AuthApi
AuthApi
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
TokenRepository
TokenRepository
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
ProductRepository
ProductRepository
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

Prefer @Binds over @Provides when binding interfaces to implementations. It generates less code and is more efficient.
Always maintain a separate OkHttp client for authentication without AuthInterceptor to prevent circular dependencies.
Network services and repositories should be singletons as they’re stateless and reusable across the app.
Never hardcode API keys or secrets. Use BuildConfig or environment variables.
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

Build docs developers (and LLMs) love