Irbisa · Cheatsheet
Android Networking
Retrofit, OkHttp, interceptors, kotlinx.serialization, error handling
What are Retrofit and OkHttp, and how do they relate?
@Serializabledata class UserDto(val id: Long, val name: String) interface UserApi { @GET("users/{id}") suspend fun user(@Path("id") id: Long): UserDto…What do
@Path,@Query,@Body,@Field,@Headerand@Urldo in a Retrofit interface?interface ShopApi { @GET("products/{id}") suspend fun product(@Path("id") id: Long): ProductDto @GET("products") suspend fun search(…execute(),enqueue()or asuspendfunction in Retrofit: which thread does each run on, and does asuspendcall needwithContext(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…The app crashes or fails on its very first network call: what are the usual causes?
<!-- 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">…How does the app know it is offline, and why is checking connectivity before every request the wrong instinct?
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) }…What changes for an Android client between HTTP/1.1, HTTP/2 and HTTP/3, and which of them does OkHttp speak?
What does
retrofit.create()return when you never wrote an implementation, and what do converters and call adapters do?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()…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) }}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}Why is kotlinx.serialization preferred over Gson in Kotlin, and how do you configure it for an API you do not control?
val json = Json { ignoreUnknownKeys = true // the backend will add fields coerceInputValues = true // null or an unknown enum value -> the default} @Serializable…What is the difference between application and network interceptors in OkHttp, and where do logging and common headers go?
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()…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()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()How do you tell network failures, HTTP errors and parsing errors apart in a Retrofit repository?
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}…The user leaves the screen while a Retrofit
suspendcall 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) } }}Which timeouts does OkHttp have, what does it retry on its own, and how do you add your own retry?
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()…How do you refresh an expired access token and retry the failed call:
Authenticatoror an interceptor?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()…How does HTTP caching work in OkHttp, and can it serve data when the device is offline?
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")…How do you upload a file with Retrofit, and how do you report upload progress?
interface UploadApi { @Multipart @POST("avatar") suspend fun upload(@Part("caption") caption: RequestBody, @Part file: MultipartBody.Part)}…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?
// 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()…Offset or cursor pagination: which do you pick, and how does Paging 3 with
RemoteMediatorload the pages?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)…How do you test the network layer without a real server?
class UserApiTest { private val server = MockWebServer() private lateinit var api: UserApi @Before fun setUp() { server.start()…REST, GraphQL or gRPC for a mobile client: what are the trade-offs?
WebSocket, Server-Sent Events, long polling or push: how do you choose the real-time transport for an Android app?
// 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…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?
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…