androidinterview.com

Android Networking Interview Questions

12 questions

Tier
Difficulty
Level

Showing all 12 questions

Retrofit & OkHttp

What is the difference between Retrofit and OkHttp, and what is the role of each?

Tier: EssentialDifficulty: Easy

OkHttp is the HTTP client that actually talks to the network, and Retrofit is a layer on top of it that turns your API into a Kotlin interface instead of hand-built requests.

  • OkHttp owns the connection. It opens sockets, handles TLS, follows redirects, retries on connection failure, and applies interceptors and caching. If you dropped Retrofit entirely, you could still make every network call OkHttp gives you directly with Request and Call objects, it's just verbose to do for a large API surface.
  • Retrofit owns the mapping. You declare an interface with annotated methods, @GET, @POST, @Body, and Retrofit generates the code that builds the right OkHttp request from your method call and converts the response body into the Kotlin type you asked for.
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

Retrofit needs an OkHttpClient to actually run, you can see that in the builder above, it's a required dependency, not an alternative. So the practical way to think about it, OkHttp is the transport layer, Retrofit is the API layer built on it. Anything about how the request goes over the wire, timeouts, headers, caching, interceptors, is configured on the OkHttpClient. Anything about what the request looks like as a Kotlin function call, endpoints, parameters, response types, is configured on the Retrofit interface.

How do you enable logging in OkHttp?

Tier: CommonDifficulty: Easy

You enable logging in OkHttp by adding HttpLoggingInterceptor from the okhttp-logging-interceptor artifact as an application interceptor on your OkHttpClient.

val logging = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.BODY
}

val client = OkHttpClient.Builder()
    .addInterceptor(logging)
    .build()

The level controls how much gets printed.

  • NONE, no logging at all, the default.
  • BASIC, just the request and response line, method, URL, status code, and timing.
  • HEADERS, adds all the headers on top of the basic line.
  • BODY, logs everything, headers and the full request and response bodies.

BODY is the one you reach for while debugging a broken API call, since it shows you exactly what went over the wire. It's also the one to strip out of release builds, because it writes full payloads including tokens and personal data straight to Logcat. The usual pattern is to only add the interceptor when BuildConfig.DEBUG is true, so production builds never carry the overhead or the leak.

Explain the OkHttp Interceptor.

Tier: CommonDifficulty: Medium

An interceptor is a piece of code that sits in the middle of every OkHttp request and response, able to observe it, rewrite it, or retry it, so you get one place to handle a concern instead of repeating it at every call site.

You implement the Interceptor interface and override intercept(), which receives a Chain you can inspect, modify, and forward.

class AuthInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(request)
    }
}

There are two kinds, and the difference is where they sit in the pipeline.

  • Application interceptors, added with addInterceptor(). They run once per call, see the request exactly as your app made it, and are the right place for auth headers, logging, or retry logic. They don't see redirects or retries OkHttp does internally.
  • Network interceptors, added with addNetworkInterceptor(). They run closer to the wire, once per actual network round trip, so they see redirects and can inspect or rewrite the connection-level request and response. Caching logic that needs to react to what the server actually sent belongs here.

Common uses are adding auth headers, logging requests and responses, injecting cache headers for offline support, refreshing an expired token before retrying, and gzip-compressing a request body. Multiple interceptors chain in the order you add them, each one calling chain.proceed() to pass control to the next, which is what lets you compose several of these concerns without any of them knowing about the others.

What is a multipart request in networking?

Tier: CommonDifficulty: Medium

A multipart request is an HTTP request whose body is split into several independent parts, each with its own headers, so you can send a mix of content, like plain text fields and a binary file, in a single call.

The classic use case is a profile update with an avatar. You need to send a JSON-ish set of fields and an image file together, and neither a plain JSON body nor a plain file upload can carry both. The multipart body solves this by giving each field its own boundary-delimited section.

In Retrofit, you build one with @Multipart on the method and one of @Part or @PartMap on the parameters.

interface ApiService {
    @Multipart
    @POST("profile")
    suspend fun updateProfile(
        @Part("name") name: RequestBody,
        @Part avatar: MultipartBody.Part
    ): Response<Unit>
}

val avatarPart = MultipartBody.Part.createFormData(
    "avatar", file.name, file.asRequestBody("image/*".toMediaType())
)

Each part gets its own Content-Type, so the file part is tagged as image/* while the text part stays plain, and the server can read them back out independently. This is why it's the standard way to handle file uploads over HTTP, not just in Android but across REST APIs generally.

Read more Send a simple request (opens in a new tab)

How does Retrofit work internally?

Tier: CommonDifficulty: Hard

Retrofit turns an interface you write into a working HTTP client by generating a dynamic proxy at runtime that translates each annotated method call into an OkHttp request, then converts the response back into the type you declared.

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

val service = retrofit.create(ApiService::class.java)

retrofit.create() never generates real bytecode for ApiService. It hands back a java.lang.reflect.Proxy, an object that implements the interface but routes every method call into a single InvocationHandler. That handler is where the real work happens, in a few steps.

  • It parses the method's annotations, @GET, @Path, @Query, @Body, and so on, once, and caches the result as a ServiceMethod so reflection only happens on the first call, not every call.
  • It builds an OkHttp Request from that parsed metadata and the arguments you passed in.
  • It hands the request to OkHttp's Call, which runs the actual network I/O.
  • It runs the raw response through a Converter, Moshi or Gson typically, to deserialize the body into your declared return type.
  • If the method is suspend, Retrofit wraps the OkHttp callback in a coroutine adapter that resumes the continuation with the result, which is what lets you call it like a normal suspend function with no manual callback handling.

The design pattern doing the heavy lifting here is the proxy pattern for the interface itself, backed by the adapter pattern for converters and call adapters, which is what lets Retrofit support Gson or Moshi, and RxJava or coroutines, without changing a line of the interface you write.

Caching & Interceptors

Interceptors are the part interviewers push on, because token refresh and retry are where a networking layer either holds together or does not.

How does HTTP caching work with OkHttp?

Tier: CommonDifficulty: Medium

OkHttp caches a response to disk when you give it a Cache and the server response carries headers that allow it, then serves the next matching request straight from that cache instead of hitting the network.

val client = OkHttpClient.Builder()
    .cache(Cache(File(context.cacheDir, "http-cache"), 10L * 1024 * 1024))
    .build()

With the cache attached, OkHttp reads the Cache-Control header the server sends back, things like max-age for how long a response is fresh, and no-store to forbid caching entirely, and honors them automatically. The catch is that many APIs don't send caching headers at all, so there's nothing for OkHttp to key off.

For those, you add a network interceptor that rewrites the response header yourself before OkHttp caches it.

class CacheInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        return response.newBuilder()
            .header("Cache-Control", "public, max-age=" + 10 * 24 * 60 * 60)
            .build()
    }
}

That alone only helps when there's a network connection, because OkHttp still tries to revalidate with the server first. To actually serve cached data while offline, you need a second, application-level interceptor that rewrites the request to force CacheControl.FORCE_CACHE whenever there's no connectivity, so OkHttp skips the network entirely and returns whatever it already has on disk. Combining a network interceptor that writes the cache headers with an application interceptor that forces the cache on read is what makes an OkHttp-based client work offline.

Read more HTTP caching (opens in a new tab)

How would you optimize handling of access token expiration, and how would you retry a network call when the API fails (custom interceptor)?

Tier: CommonDifficulty: Hard

You handle both with a custom OkHttp Authenticator for token refresh and a retrying interceptor for transient failures, kept as two separate concerns even though they both react to a failed call.

For token expiration, OkHttp's Authenticator interface is the right tool, not a plain interceptor, because it's built specifically to run once when a request comes back 401 and to hand OkHttp a new authenticated request to retry.

class TokenAuthenticator(private val tokenRepo: TokenRepository) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val newToken = tokenRepo.refreshTokenBlocking() ?: return null
        return response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
}

Two details make this safe under load.

  • Guard the refresh with a lock or a single in-flight Deferred, so if five requests fail with 401 at once, only one refresh call goes out and the other four wait on it instead of hammering the auth endpoint.
  • Return null when the refresh itself fails, which tells OkHttp to give up and surface the failure, otherwise you get an infinite retry loop against an endpoint that will never succeed.

For retrying on transient failures like a timeout or a 5xx, that's a regular interceptor, since it needs to run on every call, not just on auth failure.

class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        var response = chain.proceed(chain.request())
        while (!response.isSuccessful && attempt < maxRetries) {
            attempt++
            response.close()
            Thread.sleep(1000L * attempt) // exponential-ish backoff
            response = chain.proceed(chain.request())
        }
        return response
    }
}

Two things worth calling out in an interview. Only retry idempotent requests, retrying a POST that already succeeded server-side but timed out on the response can create a duplicate order or a duplicate payment. And back off between attempts instead of retrying immediately, otherwise a struggling server gets hit harder right when it's least able to handle it.

Protocols & Real-time

Compare HTTP Request vs HTTP Long-Polling vs WebSocket vs Server-Sent Events (REST vs WebSockets).

Tier: EssentialDifficulty: Medium

These are four ways a client and server exchange data, and the difference is who initiates the exchange and whether the connection stays open.

  • Plain HTTP request. Client opens a connection, sends a request, server responds, connection closes. Good for one-off data like loading a profile. No way to push new data without the client asking again.
  • Polling. The client just repeats a plain HTTP request on a timer, every few seconds, to check for updates. Simple to build, but it wastes requests when nothing has changed, delays updates by up to the polling interval, and drains battery from the constant wakeups.
  • Long-polling. The client sends a request and the server holds it open, not responding until it actually has new data or a timeout hits. The client immediately reopens the request once it gets a response. This cuts down on empty responses compared to polling and delivers data close to real time, but you still pay reconnection overhead on every cycle.
  • WebSocket. After one handshake, the connection stays open and both sides can send data whenever they want, in either direction. This is the only one of the four that's truly bidirectional, which makes it the right fit for chat apps or anything with continuous two-way traffic.
  • Server-Sent Events. Like WebSocket in that the connection stays open, but one-way, server to client only. Simpler to implement than a WebSocket when the client never needs to push data back, good for something like a live stock ticker or streaming tokens from an LLM.

The interview framing of "REST vs WebSockets" comes down to the same tradeoff. REST is simple, cacheable, and fine for anything request-driven. WebSocket costs more to set up and to keep alive but is the only option when the server needs to push data the moment it changes, without the client asking first.

What is the difference between a Webhook and Polling?

Tier: CommonDifficulty: Easy

A webhook is the server telling you when something happens, polling is you asking the server over and over whether something happened.

  • Polling. Your client repeatedly hits an endpoint on a timer to check for new data. It's simple and needs no setup on the server's side beyond the endpoint itself, but it wastes requests when nothing has changed and the delay before you notice an update is bounded by how often you poll.
  • Webhook. You register a URL with the server ahead of time, and it makes an HTTP call to that URL the moment an event happens, no asking required. This is close to instant and generates no wasted traffic, but it needs a publicly reachable endpoint to receive the call, which usually means a backend of your own, not something a mobile client alone can expose.

That last point is why webhooks rarely show up directly in an Android app, a phone doesn't have a stable public address to receive one. The typical setup is a webhook from a third-party service into your backend, and your backend then pushes that update to the app over FCM or a WebSocket. Polling, by contrast, is something a mobile client can do entirely on its own, which is why it still shows up for things like checking payment status after returning from a browser-based checkout flow.

What are the options for real-time updates in an Android app?

Tier: CommonDifficulty: Medium

There are five common options, and picking one is really a tradeoff between how real time you need to be and how much you're willing to spend keeping a connection alive.

  • Polling, the client asks on a timer. Simplest to build, wastes requests, and delays are bounded by the interval. Fine for something like a delivery ETA that only needs to refresh every 30 seconds.
  • Long-polling, the client asks and the server holds the request open until it has something. Closer to real time than polling with less wasted traffic, but still reconnects on every cycle.
  • WebSocket, one persistent, bidirectional connection. The right choice when the client also needs to send data continuously, like a chat app or a live multiplayer feature.
  • Server-Sent Events, one persistent, one-way connection from server to client. Simpler than WebSocket when the client never needs to push data back, like a live score or a notification feed.
  • Push notifications through Firebase Cloud Messaging. The app doesn't hold any connection itself, FCM does that at the OS level, and wakes the app with a payload when something happens. This is the right tool when updates are infrequent and the app doesn't need to be running, since it's the only option of the five that works even when the app is killed.

In practice a real app often combines these. FCM to wake the app or notify the user when it's backgrounded, and a WebSocket only while a real-time screen, like a chat thread, is actually open in the foreground. Holding a live socket connection the whole time the app is running just to handle rare updates is wasted battery and wasted server capacity.

What is MQTT, and when would you use it in an Android app?

Tier: CommonDifficulty: Medium

MQTT is a lightweight publish and subscribe messaging protocol that runs over TCP with a broker sitting in the middle. You reach for it in an Android app when many devices need to push small updates continuously to many listeners, and live driver location in a delivery or ride hailing app is the case it was made for. It is one option among several, so it belongs next to the wider set of real-time options.

Start with what the protocol actually is.

  • Publish and subscribe, never point to point. Clients never address each other. A publisher sends a message to a topic, subscribers register interest in a topic, and the broker does all the routing. Neither side needs to know the other exists.
  • Tiny on the wire. The fixed header is two bytes. A position update costs a fraction of what the same update costs as an HTTP request carrying headers and a TLS handshake.
  • Built for bad networks and small devices. It was designed in 1999 for satellite links over oil pipelines, so reconnection, session state and delivery guarantees are in the protocol rather than in your app code.
  • Two versions in the wild. MQTT 3.1.1 is the OASIS standard everything speaks. MQTT 5 added reason codes on every acknowledgement, session expiry intervals, message expiry, topic aliases, user properties and shared subscriptions. Prefer 5 when the broker supports it.

Then the pieces you should be able to name in the room.

  • The broker. One server that accepts connections, stores session and retained state, and fans messages out. It is the single thing you have to run and scale.
  • The client id. A per connection identifier the broker uses to find your session again after a reconnect. Two clients sharing one id will keep kicking each other off.
  • Topics and wildcards. Topics are slash separated strings like riders/42/location. Subscribers can use + for exactly one level and # for everything below, and # has to be the last character. Wildcards are for subscribing only, a publish always names an exact topic.
  • QoS 0, at most once. One PUBLISH, no acknowledgement, no retry. Cheapest and fastest, and the message can simply be lost.
  • QoS 1, at least once. PUBLISH then PUBACK, retried until acknowledged, so duplicates are possible and the receiver has to be idempotent.
  • QoS 2, exactly once. A four packet handshake, PUBLISH, PUBREC, PUBREL, PUBCOMP, with state held on both sides. Most expensive and slowest, worth it only for a command you must not run twice.
  • QoS is negotiated per leg. The publisher picks one, the subscriber asks for one, and the delivered level is the lower of the two.
  • Retained messages. The broker keeps the last message flagged retained on each topic and hands it to a new subscriber the instant it subscribes. Publishing an empty retained payload deletes it.
  • Last will and testament. The client hands the broker a message at connect time. The broker publishes it if the connection drops ungracefully, and discards it on a clean disconnect.
  • Keep alive and the ping. The client agrees an interval at connect. If it has nothing to send it sends a PINGREQ and gets a PINGRESP back, and the broker declares the client dead after one and a half times the interval.
  • Persistent sessions. With cleanSession false in 3.1.1, or clean start plus a session expiry interval in 5, the broker keeps your subscriptions and queues QoS 1 and 2 messages while you are offline, then delivers them on reconnect.
  • MQTT over WebSocket. The same packets wrapped in a WebSocket frame on port 443. It costs a little overhead and gets you through corporate proxies and firewalls that block the native ports.

That set is exactly why delivery and ride hailing apps land on it for live location.

  • The broker owns the fan out. Thousands of riders each publish to their own topic, and every customer subscribes to just the one rider they are waiting on. Your servers never have to compute who gets what.
  • QoS 0 or 1 for positions. A pin that is replaced a second later is not worth a four packet handshake. QoS 0 for a dense stream, QoS 1 when losing a point would visibly break the trail.
  • A retained last position. A customer who opens the app mid trip sees the current pin immediately instead of waiting for the next update.
  • A will marks the rider offline. The rider's client sets a will on its status topic, so a dropped phone flips the map to offline on its own without a heartbeat service on your side.

Compared with the alternatives, the tradeoff is narrow but real.

  • MQTT. Many to many through a broker you run, with QoS, retained state and wills built in. Best when the fan out pattern is the hard part.
  • WebSocket. One socket, bidirectional, no routing or delivery semantics. You build topics, retries and presence yourself, which is fine for one chat screen and painful at rider scale.
  • FCM. No connection of your own, and the only option that reaches a killed app. Rate limited and not ordered, so it is a wake up and notification channel, not a location stream.
  • Polling. No connection at all, easy to reason about, and wasteful past roughly one request a minute.

On Android specifically, a few implementation points earn credit.

  • Client libraries. Paho is the long standing Eclipse client, HiveMQ ships a modern async one. Both handle the packet layer, neither manages your process lifecycle for you.
  • Tie the client to a foreground service. Run it inside a foreground service with the location type while the app is genuinely tracking, and disconnect the moment it is not. Android 14 requires the type declaration and its matching permission.
  • Reconnect with exponential backoff and jitter. Mobile networks drop constantly, and a fixed retry from every client turns one broker blip into a thundering herd.
  • Keep alive costs battery. Every ping wakes the radio. A longer keep alive saves power and delays how fast the broker notices a dead client, a shorter one detects failure quickly and drains faster, so pick the interval from how stale a rider marker is allowed to be.
  • Doze will cut the socket. Once the device is idle with the screen off, network access is suspended and your connection dies. There is no keeping it alive in the background, so FCM is the wake up path and MQTT is the foreground stream.

And security is a fair follow up.

  • TLS everywhere. Plain MQTT is cleartext. Use the TLS port and pin or verify the broker certificate the same way you would an HTTPS API.
  • Per device credentials. Never one shared username and password in the APK. Issue a short lived token or a client certificate per install, so one leaked device is one revocation.
  • Topic ACLs on the broker. A rider may publish only to riders/<their id>/# and subscribe only to their own orders. Without that, anyone with a valid login can publish a fake position for any rider.
// A generic client interface, so none of this is tied to one library.
interface MqttClient {
    suspend fun connect(options: ConnectOptions)
    suspend fun subscribe(topic: String, qos: Int, onMessage: (String, ByteArray) -> Unit)
    suspend fun publish(topic: String, payload: ByteArray, qos: Int, retained: Boolean)
    suspend fun disconnect()
}

suspend fun MqttClient.startTracking(riderId: String) {
    connect(
        ConnectOptions(
            clientId = "rider-$riderId",
            keepAliveSeconds = 60,        // ping every 60s, declared dead after 90s
            cleanStart = false,           // keep subscriptions across reconnects
            will = Will(
                topic = "riders/$riderId/status",
                payload = "offline".toByteArray(),
                qos = 1,
                retained = true,          // a late subscriber still sees offline
            ),
        ),
    )

    // Wildcards are for subscribing only. + matches exactly one level.
    subscribe("riders/$riderId/orders/+", qos = 1) { topic, payload ->
        handleOrder(topic, payload)
    }

    // QoS 0, because a newer position replaces this one a second later.
    publish(
        topic = "riders/$riderId/location",
        payload = encodePosition(lat, lng),
        qos = 0,
        retained = true,                  // the broker holds the current pin
    )
}

Answer the shape first, a broker in the middle, topics instead of addresses, and QoS, retained messages and wills as the three features you would not want to rebuild. Then say where you would use it, high frequency many to many updates like live location, with FCM alongside it for the wake up, because that pairing is the answer an interviewer is listening for.

Read more Optimize for Doze and App Standby (opens in a new tab)Foreground service types (opens in a new tab)

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Protocols & Real-time

What is the difference between WebSocket and Socket.IO?

Tier: Less commonDifficulty: Medium

WebSocket is a protocol, Socket.IO is a library built on top of it, and the library adds a lot that the raw protocol doesn't give you.

  • WebSocket is the standard, a persistent, bidirectional connection defined by an RFC and supported natively by the OS and every major HTTP client. It gives you a raw duplex pipe, send and receive frames, nothing more.
  • Socket.IO is a client-server library that uses WebSocket as its transport when available, but falls back to HTTP long-polling automatically if a WebSocket connection can't be established, say behind a restrictive proxy. On top of that it adds automatic reconnection, acknowledgement callbacks for individual messages, and named events instead of raw frames.

The tradeoff is that a Socket.IO client can only talk to a Socket.IO server, since the framing and handshake it adds aren't plain WebSocket, so both ends have to opt into the library. A plain WebSocket client can talk to any WebSocket server, since it's just the protocol.

For an Android interview, the practical answer is to reach for plain WebSocket, through OkHttp's built-in WebSocket support, when you control both client and server and just need a duplex pipe. Reach for a Socket.IO client library when the backend is already built on Socket.IO, which is common in Node.js shops, since replicating its reconnection and acknowledgement behavior yourself on top of raw WebSocket is real work you'd rather not repeat.