Надежные HTTP‑запросы в Android: практическое руководство
Короткий ответ: для большинства задач используйте Retrofit + OkHttp + Kotlin Coroutines, настройте таймауты и interceptors, оборачивайте ответы в безопасный NetworkResult, применяйте ограниченный retry с backoff и храните токены безопасно — это обеспечит предсказуемую и устойчивую работу при плохом соединении.
Если коротко: Retrofit для интерфейса API, OkHttp для конфигурации (таймауты, кеш, interceptors), Coroutines для асинхронности.
Оглавление {{TOC_AUTOMATIC}}
Выбор библиотеки и базовая конфигурация клиента
- Почему Retrofit + OkHttp: декларативный API, удобные конвертеры (Moshi/kotlinx.serialization), тестируемость; OkHttp — контроль таймаутов, кеша, перехватчиков.
- Минимальная конфигурация OkHttp:
fun createOkHttpClient(tokenProvider: () -> String?): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(AuthInterceptor(tokenProvider))
.apply { if (BuildConfig.DEBUG) addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) }
.build()
- Retrofit-инстанс:
fun createRetrofit(okHttp: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttp)
.addConverterFactory(MoshiConverterFactory.create())
.build()
Не оставляйте подробное логирование тел запросов/ответов в релизных сборках — утечка чувствительных данных.
Архитектура: репозитории, безопасный вызов и retry
Организуйте сетевой слой в отдельный модуль/пакет: ApiService (интерфейсы Retrofit) → репозитории → use-cases/ViewModel. Всегда возвращайте не голый объект, а обёртку результата.
Пример sealed-класса результата:
sealed class NetworkResult<out T> {
data class Success<T>(val data: T): NetworkResult<T>()
data class HttpError(val code: Int, val message: String?): NetworkResult<Nothing>()
data class NetworkError(val throwable: Throwable): NetworkResult<Nothing>()
object UnknownError: NetworkResult<Nothing>()
}
Безопасный вызов:
suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResult<T> =
try {
val resp = apiCall()
val body = resp.body()
if (resp.isSuccessful && body != null) NetworkResult.Success(body)
else NetworkResult.HttpError(resp.code(), resp.errorBody()?.string())
} catch (e: IOException) { NetworkResult.NetworkError(e) }
catch (e: Exception) { NetworkResult.UnknownError }
Retry с exponential backoff (ограниченное число попыток):
suspend fun <T> retryWithBackoff(
maxAttempts: Int = 3,
initialDelayMs: Long = 500,
factor: Double = 2.0,
block: suspend () -> NetworkResult<T>
): NetworkResult<T> {
var delayMs = initialDelayMs
repeat(maxAttempts - 1) {
when (val r = block()) {
is NetworkResult.Success -> return r
is NetworkResult.NetworkError -> { delay(delayMs); delayMs = (delayMs * factor).toLong() }
else -> return r
}
}
return block()
}
UI: маппинг ошибок в понятные сообщения и показ retry-кнопки. Не крашить приложение при NetworkError.
Используйте Response
Кеш, офлайн и безопасность
- Кеширование: OkHttp Cache + корректные заголовки Cache-Control/ETag. Для важных данных — local DB (Room) + стратегия: сначала локальная копия, затем фоновой refresh.
- JSON: Moshi или kotlinx.serialization; отделяйте DTO от доменной модели (мэппинг на слое репозитория).
- Авторизация: добавляйте токен через Interceptor, обновление токена организуйте отдельно (чтобы избежать race conditions).
Пример простого AuthInterceptor:
class AuthInterceptor(private val tokenProvider: () -> String?): Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().newBuilder().apply {
tokenProvider()?.let { addHeader("Authorization", "Bearer $it") }
}.build()
return chain.proceed(req)
}
}
Для критичных приложений рассмотрите certificate pinning. Токены храните в secure storage (EncryptedSharedPreferences/Keystore).
Частые ошибки
- Вызов сетевых операций в UI-потоке.
- Игнорирование HTTP-кодов (работа только с телом).
- Логирование токенов/паролей в релизе.
- Жёстко захардкоженные baseUrl и ключи.
- Попытки «магически» делать refresh токена внутри одного interceptor-а без синхронизации — приводят к race conditions/циклам.
Хорошая архитектура: можно сменить сервер или HTTP-клиент с минимальными изменениями в остальной части приложения.
FAQ
- Как выбирать таймауты? — Установите connectTimeout 5–15s, read/write 15–60s, исходя из требований UX и типа операций (файловые загрузки — больше).
- Что лучше для JSON — Moshi или kotlinx.serialization? — Moshi зрелая и совместима с Retrofit; kotlinx.serialization быстрее и удобна для мультиплатформы.
- Как корректно обновлять токен? — Делайте refresh в отдельном слое; синхронизируйте запросы, ожидающие токен; не выполняйте refresh в бесконечном loop внутри interceptor-а.
- Когда включать retry? — Для коротких временных сбоев (NetworkError, timeouts). Исключите retry для явных HTTP-ошибок (4xx). Ограничьте число попыток и используйте backoff.
Примените предложенные шаблоны: обёртку NetworkResult, safeApiCall, ограниченный retry, централизованный AuthInterceptor и кеширование — и ваш сетевой слой станет предсказуемым, тестируемым и устойчивым к реальным сетевым условиям.