Developing a network layer with Retrofit often hits pitfalls: unexpected 401, parsing errors, token leaks. One of our projects—a banking service app—required reliable authentication with token refresh. Without configuring OkHttp's Authenticator, every request to a protected resource returned an error. We had to rewrite the logic to avoid manual handling in every UseCase. Over 5 years of experience on 20+ projects, we have developed a standard configuration that cuts network layer development time by 30–50%. Let's look at best practices for setting up a Retrofit network layer.
How to set up authentication in Retrofit?
Authentication is built on two components: an Interceptor to add the header and an Authenticator to refresh the token. The Interceptor reads the token from secure storage (EncryptedSharedPreferences) and attaches it to every request. When the server returns 401, the Authenticator tries to refresh the token via a refresh endpoint and retries the request. This eliminates copying auth logic throughout the project and works with any OAuth2 provider.
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer ${tokenProvider.getToken()}") .build() return chain.proceed(request) } } class TokenAuthenticator( private val tokenProvider: TokenProvider, private val refreshApi: RefreshApi ) : Authenticator { override fun authenticate(route: Route?, response: Response): Request? { synchronized(this) { val newToken = tokenProvider.getToken() ?: return null if (response.request.header("Authorization") == "Bearer $newToken") { val refreshed = refreshApi.refresh(newToken) if (refreshed.isSuccessful) { tokenProvider.saveToken(refreshed.body()!!.accessToken) return response.request.newBuilder() .header("Authorization", "Bearer ${refreshed.body()!!.accessToken}") .build() } } } return null } } Why KotlinX Serialization over Gson?
In Kotlin projects, kotlinx.serialization offers advantages: null-safety at parse level, sealed class support, and no reflection. This matters when obfuscating with R8, as Gson's reflective calls can break. In our measurements, KotlinX processes JSON 2–3x faster than Gson on payloads over 100 KB. Also, APK size increases only ~50 KB vs ~200 KB for Gson.
| Criterion | Gson | KotlinX Serialization |
|---|---|---|
| Speed (relative) | 1x | 2–3x |
| Null-safety | No | Yes |
| Sealed classes | No | Yes |
| Reflection | Yes | No |
| APK size increase | ~200 KB | ~50 KB |
OkHttp Interceptors
Most of the network layer logic concentrates here. Besides authentication, typical interceptors:
- Logging:
HttpLoggingInterceptorwith levelBODYonly for debug builds. In production—NONEto avoid logging sensitive data. - Retry: custom interceptor with exponential backoff for
IOException. Do not retry 4xx/5xx—only network failures. - Timeout:
connectTimeout(30, TimeUnit.SECONDS),readTimeout(30, TimeUnit.SECONDS),writeTimeout(30, TimeUnit.SECONDS). For file uploads, use a separate client with increasedwriteTimeout.
| Interceptor | Purpose | Example Configuration |
|---|---|---|
| AuthInterceptor | Add Bearer token | .addInterceptor(AuthInterceptor(tokenProvider)) |
| TokenAuthenticator | Auto-refresh token | .authenticator(TokenAuthenticator(tokenProvider, refreshApi)) |
| HttpLoggingInterceptor | Log requests/responses | .addInterceptor(HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) BODY else NONE }) |
| RetryInterceptor | Retry on network errors | Custom implementation with exponential backoff |
Common mistakes when setting up:
- Wrong baseUrl: must end with a trailing slash
/. - Missing
INTERNETpermission in manifest. - Token stored in
SharedPreferenceswithout encryption—useEncryptedSharedPreferences. - Forgot to add logger in debug—debugging takes hours.
Error handling
Retrofit's suspend functions throw HttpException for non-2xx statuses and IOException for network problems. Wrap in a sealed class:
sealed class ApiResult<out T> { data class Success<T>(val data: T) : ApiResult<T>() data class Error(val code: Int, val message: String) : ApiResult<Nothing>() data object NetworkError : ApiResult<Nothing>() } This lets the ViewModel handle errors in a typed way without try/catch on every call. The wrapping logic is in NetworkDataSource. For unit tests, use MockWebServer—simulate responses and verify parsing correctness. This approach reduces integration debugging time by 20–30%.
How we work on the network layer
Our process includes 5 stages:
- Analysis—define endpoints, request/response formats, and security requirements.
- Design—choose the stack (Retrofit + OkHttp + serializer), design interfaces and data models.
- Implementation—write network layer code, configure interceptors, error handling, unit tests.
- Testing—integration tests with MockWebServer, verify auth, retry, timeout scenarios.
- Deployment—integrate into CI/CD, set up productFlavors for different environments.
What's included in network layer setup work
- API documentation (format, endpoints, sample requests/responses)
- Complete network layer code (interfaces, interceptors, models)
- Unit tests and integration tests (at least 80% coverage)
- CI/CD configuration for building different environments
- Code review and recommendations for future expansion
- 2-week support guarantee after delivery
The cost of setting up a network layer varies depending on integration complexity. Typical investment: $800–$1200. Timeline: from 1 to 3 days.
How to set up Retrofit in 5 steps
- Add dependencies in
build.gradle.kts:implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")Get a consultation on setting up the network layer for your Android app. We guarantee a robust, production-ready solution backed by 5+ years of experience. Contact us to discuss your project.
Additional resources: Retrofit and OkHttp—official sources for these libraries.







