Стандартный сетевой стек 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-классов?