Перейти к содержанию
Мобильная разработка

10 вопросов по теме «Android: Сеть и хранение» на собеседовании

В этом материале — 10 вопросов из русской колоды RecallDeck по теме «Android: Сеть и хранение». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

11 мин чтения10 подробных ответовПроверено 24 августа 2026
Главная мысль

Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.

Вопросы и ответы

10 подробных ответов

01

Из чего состоит стек Retrofit + OkHttp и за что отвечает каждый слой?

Короткий ответ: Retrofit — верхний, декларативный слой: превращает Kotlin-интерфейс с аннотациями в HTTP-запросы и парсит ответ через Converter. OkHttp под ним — сам HTTP-клиент: сокеты, пул соединений, кэш, интерцепторы, таймауты, повторы. Retrofit без OkHttp не работает — это его транспорт.

Подробно:

┌─────────────────────────────────────────┐
│ ApiService (interface + @GET/@POST)      │  ← ваш код
├─────────────────────────────────────────┤
│ Retrofit: аннотации → Request,           │
│           Converter (JSON ↔ data class)  │
├─────────────────────────────────────────┤
│ OkHttp: interceptors, connection pool,   │
│         cache, timeouts, TLS, retry      │
├─────────────────────────────────────────┤
│ Socket / сеть                            │
└─────────────────────────────────────────┘
  1. Retrofit — знает про suspend и возвращает готовый data class; сам JSON не парсит.
  2. Converter — kotlinx.serialization / Moshi / Gson; именно он делает JSON ↔ объект.
  3. OkHttp — вся сетевая механика; один OkHttpClient на приложение, чтобы пул и кэш переиспользовались.
  4. CallAdapter — поддержка suspend-функций из коробки.

⚠️ Частая ошибка: создавать новый OkHttpClient/Retrofit на каждый запрос. Тогда теряются пул соединений и кэш; клиент должен быть синглтоном на всё приложение.

02

Почему SharedPreferences вытесняет DataStore, и чем Preferences DataStore отличается от Proto?

Короткий ответ: SharedPreferences делает синхронный дисковый I/O: getX при первом обращении читает весь файл в память на вызывающем потоке, а commit() пишет синхронно — на главном потоке это риск ANR. DataStore построен на Coroutines и Flow: всё асинхронно и main-safe, данные приходят через Flow, запись транзакционна.

Подробно:

SharedPreferences Preferences DataStore Proto DataStore
I/O синхронный, на вызывающем потоке async (Flow) async (Flow)
Типобезопасность нет (String-ключи) нет (ключи) да (схема .proto)
Запись apply/commit edit { } атомарно updateData атомарно
  1. Чем плох SPcommit() блокирует поток; apply() откладывает fsync, но система может синхронно дождаться его в очереди перед onPause/onStop.
  2. Preferences DataStore — те же key-value, но через Flow и корутины: drop-in замена для простых настроек.
  3. Proto DataStore — типизированная схема, ошибки ловятся на компиляции; для структурированного стейта.
  4. МиграцияSharedPreferencesMigration переносит старые ключи автоматически.

⚠️ Частая ошибка: считать apply() полностью бесплатным. Он лишь откладывает запись на диск, но Android может синхронно дождать этот fsync в lifecycle-очереди — и вы получите тот же jank.

03

Из каких частей состоит Room и почему DAO-методы делают suspend или возвращают Flow?

Короткий ответ: Три кита: @Entity (таблица), @Dao (интерфейс запросов), @Database (держит связки и версию). suspend-метод уводит запрос с главного потока (main-safety), а возврат Flow превращает запрос в реактивный стрим — Room сам переэмитит данные при изменении таблицы.

Подробно:

@Entity(tableName = "user")
data class User(@PrimaryKey val id: Long, val name: String)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun getById(id: Long): User?      // разовое чтение, вне main

    @Query("SELECT * FROM user")
    fun observeAll(): Flow<List<User>>          // реактивный стрим

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(user: User)
}
  1. Room генерит реализацию DAO (KSP/kapt) — сам пишет биндинг параметров и маппинг курсора в объект.
  2. suspend DAO — Room выполняет запрос на своём Executor, а не на main; отдельный withContext(IO) не нужен.
  3. Flow-запрос — Room следит за таблицами через InvalidationTracker и заново эмитит при любом изменении.
  4. Compile-time проверка@Query валидируется при сборке: опечатка в SQL = ошибка компиляции, а не краш в рантайме.

⚠️ Частая ошибка: сделать не-suspend DAO-метод, возвращающий List, и вызвать его на главном потоке. Room бросит IllegalStateException — «Cannot access database on the main thread».

04

Application- и network-интерцепторы OkHttp: в чём разница и как правильно обновлять токен?

Короткий ответ: Application-интерцептор вызывается один раз на логический вызов (над кэшем и редиректами), видит исходный Request и финальный Response — сюда логи и добавление заголовков. Network-интерцептор вызывается на каждый реальный сетевой хоп (редирект/повтор), видит «сырой» ответ — сюда кэш-манипуляции. Токен обновляет не интерцептор, а Authenticator: он реагирует на 401 и повторяет запрос с новым токеном.

Подробно:

App ─► [application interceptors] ─► cache ─► [network interceptors] ─► server
        видит финальный ответ,               вызывается на КАЖДЫЙ
        1 раз на вызов                        редирект / retry
val client = OkHttpClient.Builder()
    .addInterceptor(authHeaderInterceptor)    // application: кладёт токен
    .addNetworkInterceptor(loggingInterceptor)
    .authenticator { _, response ->           // срабатывает на 401
        val fresh = tokenStore.refreshBlocking() ?: return@authenticator null
        response.request.newBuilder()
            .header("Authorization", "Bearer $fresh")
            .build()
    }
    .build()
  1. Application — не видит редиректы/повторы, зато гарантированно вызывается даже при ответе из кэша; идеален для auth-заголовка и логов.
  2. Network — вызывается только при реальном выходе в сеть; видит Content-Encoding, редиректы; для кэша и низкоуровневых правок.
  3. Authenticator — реактивный: OkHttp сам зовёт его на 401 и повторяет запрос; вернуть null = сдаться. Синхронизируйте refresh (mutex), иначе десять параллельных 401 запустят десять refresh.

⚠️ Частая ошибка: обновлять токен в интерцепторе, вручную проверяя if (code == 401). Это дублирует то, что Authenticator делает штатно (с ограничением попыток), и легко зацикливается на повторных 401.

05

kotlinx.serialization, Moshi или Gson — что выбрать и где ловушки с Kotlin?

Короткий ответ: kotlinx.serialization — Kotlin-native, codegen на compile-time (без рефлексии), знает про nullability и default-значения; дефолт для нового кода. Moshi — codegen-адаптеры (KSP), тоже уважает non-null; хорош для Java/Kotlin-микса. Gson — рефлексия и легаси: игнорирует Kotlin-типы, может засунуть null в non-null поле и обойти конструктор.

Подробно:

kotlinx.serialization Moshi (codegen) Gson
Механизм codegen (compiler plugin) codegen (KSP) рефлексия
Kotlin null / defaults уважает уважает игнорирует
Статус активный, дефолт активный фактически заморожен
  1. Проблема Gson — через рефлексию он создаёт объект в обход конструктора (Unsafe), поэтому default-значения не применяются, а отсутствующее в JSON non-null поле остаётся null → NPE позже, вдалеке от места парсинга.
  2. kotlinx.serialization@Serializable + плагин; @SerialName для маппинга имён; explicitNulls, coerceInputValues настраиваются.
  3. Moshi@JsonClass(generateAdapter = true); рефлексивный режим тоже есть, но codegen быстрее и безопаснее.

⚠️ Частая ошибка: держать Gson и полагаться на дефолты в data class. Отсутствующее в JSON поле не запустит инициализатор — val page: Int = 1 окажется 0, а non-null String станет null. kotlinx/Moshi подставят дефолт или честно упадут.

06

Как обрабатывать ошибки сетевых вызовов: sealed-результат, ретраи и backoff?

Короткий ответ: Оборачивайте вызов в sealed-класс Result (Success/Error), а не разбрасывайте try/catch по UI. Различайте типы ошибок (нет сети / HTTP-код / парсинг), ретраьте только временные (IOException, 5xx, 429) с экспоненциальным backoff и джиттером, а 4xx (кроме 429) не ретраьте — повтором они не чинятся.

Подробно:

sealed interface ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>
    data class Error(val cause: Throwable, val code: Int? = null) : ApiResult<Nothing>
}

suspend fun <T> safeCall(block: suspend () -> T): ApiResult<T> = try {
    ApiResult.Success(block())
} catch (e: CancellationException) {
    throw e                                    // отмену НЕ глотаем
} catch (e: HttpException) {
    ApiResult.Error(e, e.code())
} catch (e: IOException) {
    ApiResult.Error(e)                         // нет сети / таймаут
}

suspend fun <T> retrying(times: Int = 3, block: suspend () -> T): T {
    var delayMs = 500L
    repeat(times - 1) {
        try { return block() } catch (e: IOException) { /* транзиент */ }
        delay(delayMs + Random.nextLong(200))  // backoff + jitter
        delayMs *= 2
    }
    return block()
}
  1. Классифицируйте — no-network (IOException), HTTP (4xx/5xx), сериализация. UI реагирует по-разному: кнопка «повторить» vs «проверьте ввод».
  2. Ретраить — 5xx, 429, таймауты; НЕ ретраить 400/401/404. Джиттер спасает от синхронного шторма клиентов, бьющих сервер в такт.
  3. CancellationException — всегда перевыбрасывать первым catch, иначе catch (e: Exception) сломает отмену корутины.

⚠️ Частая ошибка: catch (e: Exception) вокруг всего вызова — глотает и CancellationException (ломая structured concurrency), и баги в коде, маскируя их под «ошибку сети».

07

Миграции Room, @Transaction и индексы: что важно не сломать?

Короткий ответ: При изменении схемы поднимаете version и даёте Migration с ALTER TABLE — иначе Room упадёт с IllegalStateException. fallbackToDestructiveMigration — аварийный выход: дропает и пересоздаёт таблицы, стирая данные пользователя. @Transaction нужен там, где несколько запросов должны быть атомарны — в том числе для @Relation-выборок.

Подробно:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}
Room.databaseBuilder(ctx, AppDb::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    // .fallbackToDestructiveMigration(dropAllTables = true)  // ⚠️ стирает данные
    .build()

@Transaction
@Query("SELECT * FROM user")
fun usersWithPosts(): Flow<List<UserWithPosts>>   // @Relation → 2 запроса, атомарно
  1. Тестируйте миграции — Room экспортирует JSON-схему (exportSchema = true), а MigrationTestHelper прогоняет реальную миграцию на старой БД.
  2. fallbackToDestructiveMigration — только для dev / кэш-БД. В Room 2.6+ параметр dropAllTables теперь явный — чтобы вы осознанно соглашались на потерю данных.
  3. @Transaction@Relation делает несколько SELECT; без транзакции между ними данные могут измениться → рассинхрон. @Transaction даёт консистентный снимок.
  4. @Index — на колонки в WHERE/JOIN/foreign key; ускоряет чтение ценой чуть более медленной записи.

⚠️ Частая ошибка: положиться на fallbackToDestructiveMigration в проде «чтобы не писать миграции». При первом же изменении схемы пользователи молча теряют все локальные данные.

08

Как устроен offline-first и принцип единого источника правды (single source of truth)?

Короткий ответ: UI читает только из локальной БД (Room) — она единственный источник правды. Сеть не рисует экран напрямую, а лишь пишет свежие данные в БД; репозиторий отдаёт Flow из Room, а синхронизация (по требованию или через WorkManager) подтягивает данные и кладёт их в ту же БД. Экран работает всегда, даже без сети.

Подробно:

        ┌───────── network fetch ─────────┐
        ▼                                  │
   [ Remote API ] ──► write ──► [ Room ] ──► Flow ──► UI

                          единый источник правды
fun observeUser(id: Long): Flow<Resource<User>> = flow {
    emit(Resource.Loading)
    try {
        val remote = api.fetchUser(id)
        dao.upsert(remote.toEntity())         // пишем в БД — Flow сам переэмитит
    } catch (e: IOException) { /* остаёмся на кэше */ }
    emitAll(dao.observe(id).map { Resource.Success(it) })
}
  1. Single source of truth — экран подписан на Room; любое изменение (из сети, с другого экрана) приходит одним путём, через БД. Двух конкурирующих состояний не бывает.
  2. Запись в БД — единственный способ «показать» новые данные; UI не знает, откуда они пришли.
  3. Синхронизация — pull по требованию или периодически через WorkManager (с constraints на сеть); конфликты решает стратегия (last-write-wins, версии).
  4. Paging 3 + RemoteMediator — тот же принцип для постраничных списков: сеть наполняет БД, PagingSource читает из БД.

⚠️ Частая ошибка: показывать ответ сети прямо в UI, а в БД писать «заодно». После поворота или перезахода экран прочитает БД — и данные мигнут или разойдутся: источник правды раздвоился.

09

HTTP-кэш OkHttp (ETag, Cache-Control) против кэша в БД — когда что?

Короткий ответ: HTTP-кэш OkHttp работает на уровне ответов и управляется сервером через Cache-Control/ETag: экономит трафик (304 Not Modified) и прозрачен, но не даёт запросов, связей и офлайн-логики. БД-кэш (Room) — источник правды приложения: SQL-запросы, @Relation, реактивность, полноценный офлайн. Для offline-first нужен именно БД-кэш; HTTP-кэш — оптимизация поверх.

Подробно:

GET /user/42

OkHttp cache?  ──hit, свежий──►  вернуть из кэша (0 сети)
   │ stale + есть ETag

GET ... If-None-Match: "abc"  ──► 304 Not Modified ──► отдать кэш, продлить срок
                              └─► 200 + тело ──────────► записать в кэш
HTTP-кэш (OkHttp) БД-кэш (Room)
Уровень сырой HTTP-ответ доменные сущности
Управление сервер (Cache-Control) приложение
Запросы / связи нет да (SQL, @Relation)
Offline-first ограничен да
  1. OkHttp cache — включается через Cache(dir, size) в клиенте; работает только для GET и только если сервер разрешил заголовками. ETag → условный запрос → 304 без тела.
  2. Cache-Controlmax-age (сколько свеж), no-cache (перепроверяй через ETag), no-store (не кэшируй). Можно переопределить сетевым интерцептором.
  3. БД-кэш — когда нужны фильтры, сортировки, связи и реактивные апдейты UI; HTTP-кэш этого не умеет в принципе.

⚠️ Частая ошибка: строить offline-first на одном HTTP-кэше OkHttp. Он вытесняется по размеру непредсказуемо, не даёт запросов и реактивных обновлений — это transport-оптимизация, а не хранилище приложения.

10

Certificate/SSL pinning и шифрование данных на устройстве: как и какие риски?

Короткий ответ: Pinning привязывает соединение к известному публичному ключу (а не к любому валидному сертификату из системного стора) — защита от подменного CA и MITM. Данные at-rest шифруют ключом из Android Keystore: ключ не покидает secure hardware. Важно в 2026: EncryptedSharedPreferences и вся security-crypto объявлены deprecated (апрель 2025) — новый код идёт на DataStore + Tink.

Подробно:

// SSL pinning через OkHttp (пин — SHA-256 публичного ключа)
val pinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup
    .build()
OkHttpClient.Builder().certificatePinner(pinner).build()
  1. Как пинятCertificatePinner (OkHttp) или Network Security Config (XML). Пин — хэш публичного ключа, а не сам сертификат.
  2. Риск ротации — если сервер сменит ключ, а в приложении зашит один пин, все клиенты потеряют связь до обновления приложения. Держите backup-пин, срок годности и по возможности отдавайте пины конфигом, а не хардкодьте намертво.
  3. At-rest — ключ в Android Keystore (AES, при наличии — StrongBox), сами данные в DataStore/файле; Keystore-ключ не экспортируется из железа.
  4. EncryptedSharedPreferences — deprecated с 2025 (security-crypto 1.1.0-alpha, известные keyset-corruption краши на части OEM); мигрируйте на DataStore + Tink или поддерживаемый форк.

⚠️ Частая ошибка: захардкодить единственный пин без backup и плана ротации — это kill switch для всех пользователей в день смены сертификата. И: pinning не заменяет шифрование at-rest — это разные угрозы (канал против устройства).

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Мобильная разработка8 мин

10 вопросов по теме «Android: Компоненты и жизненный цикл» на собеседовании

10 вопросов по теме «Android: Компоненты и жизненный цикл» с подробными ответами, примерами, компромиссами и частыми ошибками кандидатов на техническом интервью.

10 подробных ответов
Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS