Урок по треку Android 7 мин

Сеть: Retrofit и OkHttp за 7 минут

Retrofit и suspend, OkHttp, интерцепторы, аутентификатор, кеш, сериализация и ошибки.

Стандартный сетевой стек Android это OkHttp (HTTP-клиент) и Retrofit (типобезопасный слой поверх него), сериализация через kotlinx.serialization или Moshi. Ktor Client встречается в Kotlin Multiplatform-проектах. На собесе спрашивают, как это устроено, интерцепторы, обновление токена и обработку ошибок.

Retrofit

interface ArticlesApi {
    @GET("articles")
    suspend fun list(@Query("page") page: Int): List<ArticleDto>

    @GET("articles/{id}")
    suspend fun byId(@Path("id") id: String): ArticleDto

    @POST("articles")
    suspend fun create(@Body body: NewArticleDto): Response<ArticleDto>
}

val json = Json { ignoreUnknownKeys = true }
val api = Retrofit.Builder()
    .baseUrl("https://api.example.com/v1/")      // со слешем на конце
    .client(okHttpClient)
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()
    .create(ArticlesApi::class.java)

Retrofit создаёт реализацию интерфейса через динамический прокси: аннотации описывают запрос, конвертер превращает тело в объекты. suspend-функции Retrofit выполняет асинхронно на потоках OkHttp, поэтому их можно звать с главного потока.

Если функция возвращает тип тела (ArticleDto), то ответ не 2xx бросит HttpException, а проблемы сети IOException. Если возвращает Response<T>, можно сам посмотреть код и заголовки, исключения только сетевые.

ignoreUnknownKeys = true важен: без него новое поле в ответе сервера уронит парсинг у всех старых версий приложения. Gson в новых проектах не советуют: он создаёт объекты в обход конструктора и может положить null в non-null поле Kotlin.

OkHttp

Один OkHttpClient на приложение: у него пул соединений и потоков, создавать клиент на каждый запрос дорого. Если нужна вариация, client.newBuilder() переиспользует пул.

Настройки: таймауты (connectTimeout, readTimeout, callTimeout), кеш (Cache(dir, size), работает по HTTP-заголовкам), certificate pinning, HTTP/2 из коробки.

Интерцепторы

Цепочка, через которую проходит каждый запрос и ответ:

class AuthInterceptor(private val tokens: TokenStore) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header("Authorization", "Bearer ${tokens.access()}")
            .build()
        return chain.proceed(request)
    }
}
  • Application interceptor (addInterceptor): вызывается один раз на вызов, видит исходный запрос, не видит редиректы и повторы. Для заголовков авторизации, логов.
  • Network interceptor (addNetworkInterceptor): вызывается на каждый сетевой запрос, включая редиректы, видит реальные заголовки (Host, сжатие), не вызывается для ответов из кеша.

HttpLoggingInterceptor подключают только в debug: в release он раскрывает токены и личные данные в логах.

Обновление токена

Authenticator вызывается, когда сервер ответил 401:

class TokenAuthenticator(private val tokens: TokenStore) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (responseCount(response) >= 2) return null        // не зацикливаться
        val newToken = synchronized(this) { tokens.refreshBlocking() } ?: return null
        return response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
}

Тонкости senior-уровня: несколько параллельных запросов получат 401 одновременно, обновлять токен должен один (синхронизация и проверка «а не обновил ли уже кто-то»); refresh-запрос делается отдельным клиентом без этого аутентификатора; при неудаче refresh пользователя разлогинивают.

Обработка ошибок

В репозитории сетевые исключения превращают в доменный результат:

suspend fun load(): Result<List<Article>> = try {
    Result.success(api.list(1).map { it.toDomain() })
} catch (e: IOException) {
    Result.failure(NoNetwork)
} catch (e: HttpException) {
    Result.failure(if (e.code() == 404) NotFound else ServerError(e.code()))
}

CancellationException не ловят (он не IOException, так что пример выше корректен). Повторы делают для идемпотентных запросов с экспоненциальной задержкой.

Безопасность

С Android 9 обычный HTTP запрещён по умолчанию (cleartext), нужен HTTPS или исключение в Network Security Config. Там же настраиваются доверенные сертификаты и pinning. Секреты (ключи API) в коде приложения не являются секретом: APK декомпилируется.

Частые ошибки

  • Новый OkHttpClient или Retrofit на каждый запрос.
  • baseUrl без слеша на конце (Retrofit бросит исключение).
  • Логирование тел запросов в release.
  • Отсутствие ignoreUnknownKeys и падение при новом поле.
  • Гонка при параллельном обновлении токена.
  • Ловля Exception целиком в корутине.

Что спросят на собесе

  • Как Retrofit создаёт реализацию интерфейса?
  • Чем application interceptor отличается от network interceptor?
  • Как реализовать обновление токена? Что будет при параллельных 401?
  • Чем Response<T> отличается от возврата тела напрямую?
  • Как кешировать ответы и как работать офлайн?
  • Почему Gson опасен для Kotlin-классов?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.