Irbisa · Шпаргалка
Сеть в Android
Retrofit, OkHttp, интерцепторы, kotlinx.serialization, обработка ошибок
Что такое Retrofit и OkHttp и как они связаны?
@Serializabledata class UserDto(val id: Long, val name: String) interface UserApi { @GET("users/{id}") suspend fun user(@Path("id") id: Long): UserDto…Что делают
@Path,@Query,@Body,@Field,@Headerи@Urlв интерфейсе Retrofit?interface ShopApi { @GET("products/{id}") suspend fun product(@Path("id") id: Long): ProductDto @GET("products") suspend fun search(…execute(),enqueue()илиsuspend-функция в Retrofit: на каком потоке работает каждый вариант и нужен лиsuspend-вызову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…Приложение падает или не работает на первом же сетевом вызове: какие обычные причины?
<!-- 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">…Как приложение узнаёт, что оно офлайн, и почему инстинкт проверять сеть перед каждым запросом неверен?
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) }…Что меняется для Android-клиента между HTTP/1.1, HTTP/2 и HTTP/3 и какие из них поддерживает OkHttp?
Что возвращает
retrofit.create(), если вы не писали реализацию, и что делают конвертеры и call adapter-ы?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()…Найдите баг: каждый запрос из этого репозитория медленный, даже когда сам сервер отвечает за 20 мс.
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) }}При такой настройке какой URL запрашивает каждый вызов и почему
users()получает 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}Почему в Kotlin kotlinx.serialization предпочитают Gson и как настроить его для API, которое вы не контролируете?
val json = Json { ignoreUnknownKeys = true // the backend will add fields coerceInputValues = true // null or an unknown enum value -> the default} @Serializable…Чем отличаются application- и network-интерсепторы в OkHttp и куда класть логирование и общие заголовки?
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()…После добавления этого интерсептора каждый вызов падает с
IllegalStateException: closed. Почему и как логировать тело правильно?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()Найдите баг: сервер отвечает 401 на каждый вызов, хотя этот интерсептор добавляет access-токен.
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()Как в Retrofit-репозитории отличить сетевые сбои, HTTP-ошибки и ошибки парсинга?
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}…Пользователь уходит с экрана, пока
suspend-вызов Retrofit в полёте: что происходит с HTTP-вызовом и откатывается ли отменённый POST?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) } }}Какие таймауты есть в OkHttp, что он повторяет сам и как добавить свой 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()…Как обновить истёкший access-токен и повторить упавший вызов: через
Authenticatorили интерсептор?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()…Как работает HTTP-кэширование в OkHttp и может ли оно отдавать данные, когда устройство офлайн?
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")…Как загрузить файл через Retrofit и как показать прогресс загрузки?
interface UploadApi { @Multipart @POST("avatar") suspend fun upload(@Part("caption") caption: RequestBody, @Part file: MultipartBody.Part)}…Список картинок из сети тормозит при прокрутке, а память растёт: что за вас делает загрузчик вроде Coil и что остаётся на вас?
// 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 или по курсору: что выбрать и как Paging 3 с
RemoteMediatorзагружает страницы?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)…Как тестировать сетевой слой без настоящего сервера?
class UserApiTest { private val server = MockWebServer() private lateinit var api: UserApi @Before fun setUp() { server.start()…REST, GraphQL или gRPC для мобильного клиента: в чём компромиссы?
WebSocket, Server-Sent Events, long polling или push: как выбрать транспорт реального времени для Android-приложения?
// 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…Загрузка файла на 400 МБ на части телефонов падает по памяти и начинается с нуля при каждом обрыве соединения: как писать её потоком на диск и докачивать?
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…