Overview
The network layer provides robust, production-ready components for making secure API calls to the Mercado Libre API. It includes automatic token management, comprehensive error handling, and request/response logging.
SafeApiCallExecutor
Orchestrator for safe network operations that handles exceptions, validates responses, and logs errors.
Purpose
SafeApiCallExecutor is the core component responsible for executing suspended API calls with comprehensive error handling. It follows the Single Responsibility Principle (SRP) by managing:
Exception handling via ErrorMapper
HTTP response validation via HttpResponseHandler
Error logging via NetworkLogger
Execution on Dispatchers.IO to avoid blocking the main thread
Implementation
package com.alcalist.tecmeli.core.network.executor
import com.alcalist.tecmeli.core.network.AppError
import com.alcalist.tecmeli.core.network.handler.HttpResponseHandler
import com.alcalist.tecmeli.core.network.logger.NetworkLogger
import com.alcalist.tecmeli.core.network.mapper.ErrorMapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Response
class SafeApiCallExecutor (
private val responseHandler: HttpResponseHandler ,
private val errorMapper: ErrorMapper ,
private val logger: NetworkLogger
) {
suspend fun < T , R > execute (
call: suspend () -> Response < T > ,
transform: ( T ) -> R
): Result < R > = withContext (Dispatchers.IO) {
try {
val response = call ()
val result = responseHandler. handleResponse (response, transform)
if (result.isFailure && response.isSuccessful. not ()) {
val error = result. exceptionOrNull ()
if (error is AppError.Server) {
logger. logServerError (error.code, error.message)
}
}
result
} catch (e: Exception ) {
val appError = errorMapper. mapException (e)
logger. logUnexpectedError (appError.javaClass.simpleName, e)
Result. failure (appError)
}
}
}
Parameters
Handler for validating and transforming HTTP responses
Mapper for converting exceptions to application errors
System for logging technical and server failures
Usage Example
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) }
}
)
}
}
Method: execute()
The type of the Retrofit response body
The desired domain type after transformation
call
suspend () -> Response<T>
Suspended lambda representing the Retrofit API call
Lambda to map from T to R
Encapsulates success with R or failure with AppError
TokenAuthenticator
OkHttp Authenticator that handles automatic token refresh when the server returns a 401 Unauthorized response.
Purpose
When the server responds with a 401 error, this component intercepts the response and attempts to refresh the access token synchronously via the TokenRepository. If successful, it retries the original request with the new token.
Implementation
package com.alcalist.tecmeli.core.network
import com.alcalist.tecmeli.domain.repository.TokenRepository
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject
class TokenAuthenticator @Inject constructor (
private val tokenRepository: TokenRepository
) : Authenticator {
override fun authenticate (route: Route ?, response: Response ): Request ? {
if (response.code != 401 ) return null
val result = runBlocking {
tokenRepository. refreshToken ()
}
return if (result.isSuccess) {
val newToken = result. getOrNull ()
response.request. newBuilder ()
. header ( "Authorization" , "Bearer $newToken " )
. build ()
} else {
null
}
}
}
Parameters
Repository for token management and refresh operations
Method: authenticate()
The response that caused the authentication challenge (expected 401)
A new Request with the updated token, or null if refresh fails
The authenticator uses runBlocking to perform synchronous token refresh, as required by OkHttp’s Authenticator interface.
AuthInterceptor
OkHttp Interceptor that injects the access token into outgoing HTTP requests.
Purpose
Intercepts every HTTP request directed to the Mercado Libre API and adds the Authorization: Bearer {token} header if a token is available in the TokenRepository.
Implementation
package com.alcalist.tecmeli.core.network
import com.alcalist.tecmeli.domain.repository.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
class AuthInterceptor @Inject constructor (
private val tokenRepository: TokenRepository
) : Interceptor {
override fun intercept (chain: Interceptor .Chain): Response {
val originalRequest = chain. request ()
// Avoid adding token for authentication endpoint
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 ())
}
}
Parameters
Repository for retrieving the current access token
Method: intercept()
The OkHttp interceptor chain
The server response after processing the request with the token
The interceptor skips adding the token for requests to the oauth/token endpoint to prevent infinite loops during token refresh.
AppError
Sealed class hierarchy defining application-level errors, decoupling technical exceptions from business logic and UI.
Error Types
sealed class AppError : Exception () {
/** Network connectivity error (no internet or connection issues) */
class Network : AppError ()
/** Connection timeout */
class Timeout : AppError ()
/** Server error (4xx, 5xx) */
data class Server ( val code: Int , val msg: String ) : AppError ()
/** Unclassified or unexpected error */
data class Unknown ( val throwable: Throwable ) : AppError ()
}
Usage Example
when ( val error = result. exceptionOrNull ()) {
is AppError.Network -> showMessage ( "No internet connection" )
is AppError.Timeout -> showMessage ( "Request timed out" )
is AppError.Server -> showMessage ( "Server error: ${ error.msg } " )
is AppError.Unknown -> showMessage ( "Unexpected error occurred" )
}
HttpResponseHandler
Defines a contract for managing and validating HTTP responses from Retrofit.
interface HttpResponseHandler {
fun < T , R > handleResponse (
response: Response < T >,
transform: ( T ) -> R
): Result < R >
}
ErrorMapper
Defines a contract for transforming technical exceptions to domain errors, following the Dependency Inversion Principle (DIP).
interface ErrorMapper {
fun mapException (exception: Exception ): AppError
}
NetworkLogger
Interface for logging network events and errors.
interface NetworkLogger {
fun logServerError (code: Int , message: String )
fun logUnexpectedError (errorType: String , exception: Exception )
}
Best Practices
Always use SafeApiCallExecutor for network calls
Don’t make direct Retrofit calls in repositories. Always wrap them with SafeApiCallExecutor to ensure consistent error handling and logging.
Handle all AppError types in UI
Implement exhaustive when statements to handle all AppError variants and provide appropriate user feedback.
Token refresh is automatic
You don’t need to manually handle 401 errors. The TokenAuthenticator handles token refresh automatically.
Separate auth and API clients
Never use AuthInterceptor on the authentication client to avoid infinite loops during token refresh.
See Also
DI Modules - Hilt configuration for network components
Utilities - UiState for managing UI state with network results