Irbisa · Cheatsheet

Android Networking

Retrofit, OkHttp, interceptors, kotlinx.serialization, error handling

Middle Developer · 25 questions

  1. 01

    What are Retrofit and OkHttp, and how do they relate?

    OkHttp is the HTTP client that actually moves the bytes; Retrofit sits on top of it and turns an annotated Kotlin interface into typed calls that OkHttp executes.

    @Serializabledata class UserDto(val id: Long, val name: String) interface UserApi {  @GET("users/{id}")  suspend fun user(@Path("id") id: Long): UserDto…
  2. 02

    What do @Path, @Query, @Body, @Field, @Header and @Url do in a Retrofit interface?

    They tell Retrofit where each argument goes in the HTTP request (the URL path, the query string, the body, a form field or a header), while @Url replaces the whole URL.

    interface ShopApi {  @GET("products/{id}")  suspend fun product(@Path("id") id: Long): ProductDto   @GET("products")  suspend fun search(…
  3. 03

    execute(), enqueue() or a suspend function in Retrofit: which thread does each run on, and does a suspend call need withContext(Dispatchers.IO)?

    execute() blocks the calling thread, enqueue() runs the call on OkHttp's thread pool and calls you back, and a suspend function uses enqueue() under the hood, so it is main-safe and needs no withContext(Dispatchers.IO).

    interface UserApi {  @GET("users/{id}")  suspend fun user(@Path("id") id: Long): UserDto            // suspends, never blocks   @GET("users/{id}")  fun userCall(@Path("id") id: Long): Call<UserDto>          // you choose execute or enqueue…
  4. 04

    The app crashes or fails on its very first network call: what are the usual causes?

    Four causes cover almost every case: the call runs on the main thread, the INTERNET permission is missing, the URL is plain http and cleartext is blocked, or localhost points at the emulator itself.

    <!-- AndroidManifest.xml --><uses-permission android:name="android.permission.INTERNET" /> <!-- src/debug/res/xml/network_security_config.xml: debug builds only --><network-security-config>  <domain-config cleartextTrafficPermitted="true">…
  5. 05

    How does the app know it is offline, and why is checking connectivity before every request the wrong instinct?

    You observe connectivity with a ConnectivityManager network callback, but you never gate a request on it: make the call and treat an IOException as the offline signal.

    fun ConnectivityManager.isOnlineFlow(): Flow<Boolean> = callbackFlow {  val callback = object : ConnectivityManager.NetworkCallback() {    override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {      trySend(caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))    }    override fun onLost(network: Network) { trySend(false) }…
  6. 06

    What changes for an Android client between HTTP/1.1, HTTP/2 and HTTP/3, and which of them does OkHttp speak?

    HTTP/1.1 carries one request at a time per connection, HTTP/2 multiplexes many streams over one TLS connection, and HTTP/3 does the same over QUIC on UDP; OkHttp speaks HTTP/1.1 and HTTP/2, but not HTTP/3.

  7. 07

    What does retrofit.create() return when you never wrote an implementation, and what do converters and call adapters do?

    It returns a java.lang.reflect.Proxy implementing your interface: every method call goes to one handler that parses that method's annotations once, builds an OkHttp request, and uses a converter for the body and a call adapter for the return type.

    val retrofit = Retrofit.Builder()  .baseUrl("https://api.example.com/")  .addConverterFactory(ScalarsConverterFactory.create())   // first: plain String bodies  .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))  .validateEagerly(BuildConfig.DEBUG)                      // parse every method at create()  .build()…
  8. 08

    Find the bug: every request from this repository is slow, even when the server itself answers in 20 ms.

    class UserRepository(private val context: Context, private val tokens: TokenStore) {  suspend fun user(id: Long): UserDto {    val client = OkHttpClient.Builder()      .cache(Cache(File(context.cacheDir, "http"), 10L * 1024 * 1024))      .addInterceptor(AuthInterceptor(tokens))      .build()    val api = Retrofit.Builder()      .baseUrl("https://api.example.com/")      .client(client)      .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))      .build()      .create<UserApi>()    return api.user(id)  }}

    Every call builds a new OkHttpClient and Retrofit, and with them a new connection pool, thread pool and cache, so no connection is ever reused, each request pays a fresh TCP + TLS handshake, and idle sockets and threads pile up.

  9. 09

    With this setup, which URL does each call request, and why does users() get a 404?

    val retrofit = Retrofit.Builder()  .baseUrl("https://api.example.com/v2/")  .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))  .build() interface UserApi {  @GET("/users")  suspend fun users(): List<UserDto>   @GET("users/{id}")  suspend fun user(@Path("id") id: String): UserDto}

    Retrofit resolves the annotation path against baseUrl the way a browser resolves a link: a leading / means "from the host root", so @GET("/users") requests https://api.example.com/users and loses /v2.

  10. 10

    Why is kotlinx.serialization preferred over Gson in Kotlin, and how do you configure it for an API you do not control?

    It generates serializers at compile time, so it respects Kotlin nullability and default values, needs no reflection or R8 keep rules and works in multiplatform code; for a foreign API you make the Json instance tolerant of extra, missing and unexpected values.

    val json = Json {  ignoreUnknownKeys = true      // the backend will add fields  coerceInputValues = true      // null or an unknown enum value -> the default} @Serializable…
  11. 11

    What is the difference between application and network interceptors in OkHttp, and where do logging and common headers go?

    Both implement Interceptor, but an application interceptor runs once per call on the logical request, while a network interceptor runs on every real round trip (each redirect and retry) and sees the request as it goes on the wire.

    class HeadersInterceptor(private val tokens: TokenStore) : Interceptor {  override fun intercept(chain: Interceptor.Chain): Response {    val request = chain.request().newBuilder()      .header("Accept-Language", Locale.getDefault().toLanguageTag())      .apply { tokens.accessToken?.let { header("Authorization", "Bearer $it") } }      .build()…
  12. 12

    After this interceptor was added, every call fails with IllegalStateException: closed. Why, and how should it log the body?

    class BodyLogger : Interceptor {  override fun intercept(chain: Interceptor.Chain): Response {    val response = chain.proceed(chain.request())    Log.d("api", response.body?.string().orEmpty())    return response  }} val client = OkHttpClient.Builder()  .addInterceptor(BodyLogger())  .build()

    A ResponseBody is a one-shot stream over the open connection: string() reads it to the end and closes it, so when Retrofit's converter later reads the same body it finds a closed stream.

  13. 13

    Find the bug: the server answers 401 to every call, although this interceptor adds the access token.

    class AuthInterceptor(private val tokens: TokenStore) : Interceptor {  override fun intercept(chain: Interceptor.Chain): Response {    val request = chain.request()    val token = tokens.accessToken()    if (token != null) {      request.newBuilder()        .header("Authorization", "Bearer $token")        .build()    }    return chain.proceed(request)  }} val client = OkHttpClient.Builder()  .addInterceptor(AuthInterceptor(tokens))  .build()

    Request is immutable: newBuilder()…build() returns a new request with the header, but the code discards it and proceeds with the original, so no call ever carries Authorization and the server answers 401.

  14. 14

    How do you tell network failures, HTTP errors and parsing errors apart in a Retrofit repository?

    A throwing suspend call fails in three different ways: IOException (no connection, timeout), HttpException (the server answered non-2xx) and SerializationException (the body does not match the model), and the repository maps each to a domain error once.

    sealed interface AppError {  data object Offline : AppError  data object Unauthorized : AppError  data class Server(val code: Int, val body: String?) : AppError  data class Contract(val cause: Throwable) : AppError}…
  15. 15

    The user leaves the screen while a Retrofit suspend call is in flight: what happens to the HTTP call, and is a cancelled POST rolled back?

    fun load(id: Long) {  viewModelScope.launch {    try {      val user = repo.user(id)            // a Retrofit suspend call inside      _state.value = UiState.Loaded(user)    } catch (e: Exception) {      _state.value = UiState.Failed(e.message)      analytics.logError("user_load_failed", e)    }  }}

    viewModelScope is cancelled, Retrofit calls call.cancel(), OkHttp closes the socket (HTTP/1.1) or resets the stream (HTTP/2) and the coroutine gets CancellationException, but the server may already have processed the request.

  16. 16

    Which timeouts does OkHttp have, what does it retry on its own, and how do you add your own retry?

    OkHttp has connect, read, write and call timeouts; on its own it only retries a failed connection attempt (another route or a fresh connection) and follows redirects; retrying a request the server may already have processed is your decision.

    val client = OkHttpClient.Builder()  .connectTimeout(10, TimeUnit.SECONDS)  .readTimeout(15, TimeUnit.SECONDS)      // inactivity between bytes  .callTimeout(30, TimeUnit.SECONDS)      // the only bound on the whole call  .build()…
  17. 17

    How do you refresh an expired access token and retry the failed call: Authenticator or an interceptor?

    Use OkHttp's Authenticator: it is called only when the server answers 401, returns the request to retry with the new token (or null to give up), and OkHttp replays it for you; a refresh hidden in an interceptor easily loops or races.

    class TokenAuthenticator(private val session: Session) : Authenticator {  override fun authenticate(route: Route?, response: Response): Request? {    if (response.priorResponse?.code == 401) return null      // the retry failed too: stop    val sent = response.request.header("Authorization")?.removePrefix("Bearer ")    val fresh = runBlocking { session.refreshIfStale(sent) } ?: return null    return response.request.newBuilder().header("Authorization", "Bearer $fresh").build()…
  18. 18

    How does HTTP caching work in OkHttp, and can it serve data when the device is offline?

    OkHttp has an opt-in disk cache (Cache(dir, maxSize) on the client) that follows standard HTTP caching (Cache-Control, ETag revalidation) and serves stale responses offline only for requests sent with only-if-cached.

    val client = OkHttpClient.Builder()  .cache(Cache(File(context.cacheDir, "http"), 50L * 1024 * 1024))   // 50 MB  .addNetworkInterceptor { chain ->    // the server sends no Cache-Control: allow 5 minutes of reuse    chain.proceed(chain.request()).newBuilder()      .header("Cache-Control", "public, max-age=300")…
  19. 19

    How do you upload a file with Retrofit, and how do you report upload progress?

    Mark the method @Multipart and pass the file as a MultipartBody.Part built from a streaming RequestBody; progress comes from wrapping that body and counting bytes as OkHttp writes them.

    interface UploadApi {  @Multipart  @POST("avatar")  suspend fun upload(@Part("caption") caption: RequestBody, @Part file: MultipartBody.Part)}…
  20. 20

    A list of remote images scrolls badly and memory keeps climbing: what does an image loader like Coil do for you, and what is still on you?

    An image loader downloads off the main thread, decodes the bitmap at the size you display rather than the source size, keeps memory and disk caches, and cancels a request when its item leaves the screen.

    // Built on the same OkHttp stack: auth, connection poolval imageLoader = ImageLoader.Builder(context)  .components { add(OkHttpNetworkFetcherFactory(callFactory = { okHttpClient })) }  .memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.25).build() }  .diskCache {    DiskCache.Builder().directory(context.cacheDir.resolve("images").toOkioPath()).build()…
  21. 21

    Offset or cursor pagination: which do you pick, and how does Paging 3 with RemoteMediator load the pages?

    Cursor pagination is the safe default for a feed that changes, because offset pages shift when rows are inserted; Paging 3 loads pages through a PagingSource, and RemoteMediator writes network pages into Room so the list also works offline.

    class PostsPagingSource(private val api: PostsApi) : PagingSource<String, Post>() {  override suspend fun load(params: LoadParams<String>): LoadResult<String, Post> = try {    val page = api.posts(after = params.key, limit = params.loadSize)    LoadResult.Page(data = page.items, prevKey = null, nextKey = page.nextCursor)  } catch (e: IOException) {    LoadResult.Error(e)…
  22. 22

    How do you test the network layer without a real server?

    Run Retrofit against MockWebServer (a real HTTP server on localhost), so OkHttp, your interceptors, the converter and the error mapping all execute, and assert both the request your client sent and how it handled the scripted reply.

    class UserApiTest {  private val server = MockWebServer()  private lateinit var api: UserApi   @Before fun setUp() {    server.start()…
  23. 23

    REST, GraphQL or gRPC for a mobile client: what are the trade-offs?

    REST is the simple, cache-friendly default; GraphQL lets a screen fetch exactly the fields it needs in one round trip; gRPC brings strict Protobuf contracts, compact binary payloads and streaming, at the cost of tooling and HTTP caching.

  24. 24

    WebSocket, Server-Sent Events, long polling or push: how do you choose the real-time transport for an Android app?

    WebSocket for two-way traffic such as chat, SSE for a one-way stream from the server, long polling only as a fallback, and FCM for anything that must reach the device while the app is not running.

    // Socket as a Flow: opened on collect, closed when the last collector goes awayfun OkHttpClient.messages(url: String): Flow<String> = callbackFlow {  val socket = newWebSocket(Request.Builder().url(url).build(), object : WebSocketListener() {    override fun onMessage(webSocket: WebSocket, text: String) { trySend(text) }    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {      webSocket.close(code, null)   // answer the server's close, or onClosed never fires…
  25. 25

    A 400 MB download runs out of memory on some phones and restarts from zero whenever the connection drops: how do you stream it to disk and resume?

    @Streaming stops Retrofit from buffering the whole body in memory, and a Range request starting at the size of the partial file on disk lets the download resume after a dropped connection.

    interface FilesApi {  @Streaming                                   // live stream, no buffering  @GET("files/{name}")  suspend fun download(    @Path("name") name: String,    @Header("Range") range: String?,           // null headers are omitted…