Networking cheat sheet
The 8 Networking interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Retrofit suspend calls are already main-safe.
- One OkHttpClient for the app; Authenticator for 401s.
- Refresh tokens once under a lock.
- Retry transient errors only, with backoff and jitter.
- Show cached data offline; queue writes with WorkManager.
Retrofit with coroutines
Declare an interface with annotated suspend functions, like @GET("users/{id}") suspend fun user(@Path("id") id: String): UserDto, and build it with Retrofit.Builder plus a converter such as kotlinx.serialization. Calling it from a coroutine is main-safe because OkHttp does the I/O on its own threads. Non-2xx responses throw HttpException; network failures throw IOException.
@Serializable data class UserDto(val id: String, val name: String)
interface UserService {
@GET("users/{id}")
suspend fun user(@Path("id") id: String): UserDto
}
fun userService(client: OkHttpClient): UserService = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType()))
.build()
.create(UserService::class.java)OkHttp interceptors
An interceptor sees each request and response and can change, retry or short-circuit them by calling chain.proceed(request). Application interceptors (addInterceptor) run once per call, even when the response comes from cache, and are good for auth headers and logging. Network interceptors (addNetworkInterceptor) run for each network request, including redirects, and see the real headers sent on the wire.
class AuthHeader(private val token: () -> String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = token()?.let { chain.request().newBuilder().header("Authorization", "Bearer $it").build() }
?: chain.request()
return chain.proceed(request)
}
}Refreshing an expired token once
Use an OkHttp Authenticator, which runs when a response is 401, and guard the refresh with a lock. The first caller refreshes; the others wait, then see that the token has already changed and just retry with the new one. If the refresh itself fails, return null so the requests fail and the app signs the user out.
interface TokenStore {
var accessToken: String?
val refreshToken: String
}
interface AuthApi {
fun refreshBlocking(refreshToken: String): String?
}kotlinx.serialization vs Gson
Gson uses reflection and doesn't understand Kotlin's null safety or default values: a missing field can leave a non-null property null, causing crashes far from the parsing code. kotlinx.serialization generates serializers at compile time with @Serializable, respects defaults and nullability, works in Multiplatform code and needs no reflection rules for R8. Moshi with codegen is another Kotlin-aware choice.
Retries with exponential backoff
Retry only errors that can succeed later: timeouts, connection failures, 5xx and 429. Don't retry 4xx errors or non-idempotent requests like payments unless the server supports idempotency keys. Wait longer each time (exponential backoff: 1 s, 2 s, 4 s) with random jitter so thousands of clients don't retry together, and respect a Retry-After header.
suspend fun <T> withRetry(times: Int = 3, block: suspend () -> T): T {
var delayMs = 1_000L
repeat(times - 1) {
try { return block() } catch (e: java.io.IOException) { }
delay(delayMs + kotlin.random.Random.nextLong(0, delayMs / 2))
delayMs *= 2
}
return block()
}HTTP caching with OkHttp
Give OkHttpClient a Cache(directory, maxSize). Then OkHttp follows the server's Cache-Control headers: fresh responses come from disk without a request, and stale ones are revalidated with If-None-Match/If-Modified-Since, so an unchanged resource costs a 304 with no body. The server's headers decide behaviour; the client just needs the cache configured.
Image loading
It downloads, decodes and caches images in memory and on disk, downsamples them to the size of the target view, reuses bitmaps, cancels loads when a row scrolls away, and handles placeholders and errors. You still need to request the right size, avoid loading full-resolution images into thumbnails, and give stable keys so lists don't reload images.
@Composable
fun Avatar(url: String) {
AsyncImage(
model = url,
contentDescription = null,
modifier = Modifier.size(48.dp),
)
}Handling offline and flaky networks
Show cached data instead of an error page, mark it as possibly out of date, and queue user changes locally so they sync later with WorkManager and a network constraint. Don't rely on 'is online' checks before requests: a connected network can still fail. Try the request, handle IOException, and react to connectivity changes to retry.
Quick facts
- HttpException
- Retrofit throws HttpException for non-2xx responses when a suspend function returns the body type.
- OkHttp call timeout
- callTimeout limits the whole call, including retries and redirects; connect/read/write timeouts limit each phase.
- Retry-After
- A 429 or 503 response can include Retry-After with the seconds to wait.
- Coil
- Coil is an image loader built on Kotlin coroutines, with AsyncImage for Compose.