Надежные HTTP‑запросы в Android: практическое руководство

Короткий ответ: для большинства задач используйте Retrofit + OkHttp + Kotlin Coroutines, настройте таймауты и interceptors, оборачивайте ответы в безопасный NetworkResult, применяйте ограниченный retry с backoff и храните токены безопасно — это обеспечит предсказуемую и устойчивую работу при плохом соединении.

Если коротко: Retrofit для интерфейса API, OkHttp для конфигурации (таймауты, кеш, interceptors), Coroutines для асинхронности.

Оглавление {{TOC_AUTOMATIC}}

Выбор библиотеки и базовая конфигурация клиента

  1. Почему Retrofit + OkHttp: декларативный API, удобные конвертеры (Moshi/kotlinx.serialization), тестируемость; OkHttp — контроль таймаутов, кеша, перехватчиков.
  2. Минимальная конфигурация 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()
  1. 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 в сигнатурах Retrofit-методов, чтобы иметь доступ к HTTP-коду и errorBody.

Кеш, офлайн и безопасность

  • Кеширование: 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 и кеширование — и ваш сетевой слой станет предсказуемым, тестируемым и устойчивым к реальным сетевым условиям.