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

10 вопросов по теме «Android: Архитектура» на собеседовании

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

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

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

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

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

01

Что такое MVVM и почему ViewModel не должна ссылаться на View или Context?

Короткий ответ: MVVM делит экран на три роли: Model (данные и логика), View (Activity/Fragment/Composable — только рисует и передаёт действия) и ViewModel (держит состояние и переживает пересоздание View). ViewModel не знает про конкретную View — она лишь публикует состояние, на которое View подписывается.

Подробно:

┌────────┐  подписка на state   ┌───────────┐  запрос данных  ┌───────┐
│  View  │ ◄─────────────────── │ ViewModel │ ──────────────► │ Model │
│ (тупая)│ ── действия (события) ─►│ (state)   │ ◄─── данные ──── │(repo) │
└────────┘                       └───────────┘                 └───────┘
  1. View — тупой рендер — берёт готовое состояние и рисует; никакой бизнес-логики, только «показать то, что дали».
  2. ViewModel — держатель состояния — переживает поворот экрана (её создаёт ViewModelStoreOwner, а не пересоздаёт вместе с Activity).
  3. Model — репозитории, БД, сеть; ViewModel обращается к ним, а не View напрямую.

⚠️ Частая ошибка: хранить View, Activity или Context в поле ViewModel. ViewModel живёт дольше View — после поворота ссылка указывает на уничтоженную Activity, и это утечка памяти. Нужен Context — берите AndroidViewModel.getApplication() или инжектите @ApplicationContext.

02

Что такое однонаправленный поток данных (UDF): «состояние вниз, события вверх»?

Короткий ответ: UDF означает, что состояние течёт в одну сторону — из ViewModel в UI (обычно через StateFlow), а события пользователя идут обратно вверх вызовами методов ViewModel. UI сам состояние не меняет: он лишь просит ViewModel, та пересчитывает состояние, UI перерисовывается.

Подробно:

           state (вниз)
   ViewModel ──────────────► UI
      ▲                       │
      └────────── events ─────┘
            (вверх: onClick, onRefresh…)
  1. Состояние — вниз — единый неизменяемый UiState спускается в UI; UI — чистая функция от состояния.
  2. События — вверх — тап, свайп, ввод текста превращаются в вызовы viewModel.onX(); только ViewModel имеет право менять состояние.
  3. Один источник правды — раз состояние правит одно место, поведение предсказуемо и легко тестируется: подал состояние — проверил отрисовку.

⚠️ Частая ошибка: позволять UI мутировать состояние напрямую (менять поля модели из обработчика клика). Тогда правда «живёт» в двух местах — в UI и во ViewModel — и они рассинхронизируются. UI только сообщает о намерении, меняет состояние ViewModel.

03

MVVM, MVP и MVI — в чём разница и почему на сложных экранах выбирают MVI?

Короткий ответ: MVP — Presenter императивно дёргает View через интерфейс (view.showLoading(), view.showError()); связь тесная, состояние размазано по вызовам. MVVM — ViewModel публикует состояние, View подписывается, но состояние часто разбито на несколько LiveData/полей. MVI доводит UDF до предела: одно неизменяемое состояние + поток интентов, поэтому «невозможных» комбинаций UI не бывает.

Подробно:

Паттерн Как обновляется UI Главная боль
MVP Presenter вызывает методы View по интерфейсу ручная синхронизация экрана, десятки методов View, тяжело при повороте
MVVM View подписан на observable-поля VM состояние размазано по нескольким потокам → рассинхрон
MVI один State + Intent, редьюсер пересчитывает всё больше бойлерплейта на простых экранах
  1. Почему MVI выигрывает на сложном — единый снимок состояния исключает противоречия (нельзя одновременно loading и error), даёт воспроизводимость и удобный лог/time-travel.
  2. Цена MVI — церемония: intent-ы, редьюсеры, эффекты. На форме из двух полей это оверкилл — там хватит MVVM.

⚠️ Частая ошибка: боль MVP — рассинхрон вида view.showLoading() вызвали, а hideLoading() в одной из веток забыли, и спиннер завис. MVI убирает это классом: экран пересобирается целиком из одного состояния.

04

Как моделировать состояние экрана: sealed interface или набор булевых флагов?

Короткий ответ: Отдельные флаги (isLoading, error, data) допускают бессмысленные комбинации — можно оказаться в isLoading = true и одновременно с непустой error. sealed interface делает невозможные состояния непредставимыми: экран может быть ровно в одном из перечисленных состояний.

Подробно:

// ❌ флаги: 2^3 комбинаций, часть из них невалидна
data class State(
    val isLoading: Boolean = false,
    val error: String? = null,
    val data: List<Item>? = null,   // loading + error + data одновременно?
)

// ✓ sealed interface: одно состояние в каждый момент
sealed interface UiState {
    data object Loading : UiState
    data class Success(val items: List<Item>) : UiState
    data class Error(val message: String) : UiState
}
  1. Невозможное — непредставимо — компилятор гарантирует, что Success несёт данные, а Error — сообщение; забыть «сбросить» флаг нельзя.
  2. when исчерпывающий — по sealed-типу компилятор требует обработать все ветки: добавили состояние — UI не соберётся, пока его не отрисуешь.
  3. Гибрид на практике — часто удобнее data class с постоянными полями (заголовок, уже загруженный список) плюс вложенный sealed для изменчивого content-состояния.

⚠️ Частая ошибка: держать data, error и isLoading тремя независимыми полями и потом в UI писать лестницу if (isLoading) … else if (error != null) …. Порядок веток становится «архитектурой», и любой рассинхрон флагов рисует битый экран.

05

Как отдавать одноразовые события (навигация, показать снекбар) из ViewModel?

Короткий ответ: Одноразовое событие — это не состояние. Класть его в StateFlow нельзя: после поворота новый подписчик получит последнее значение и событие сработает повторно (двойная навигация, двойной тост). Варианты — Channel().receiveAsFlow() (ровно один consumer, гарантированная доставка) либо, где возможно, выразить эффект через само состояние.

Подробно:

Механизм Свойство Риск
StateFlow всегда хранит последнее значение событие повторится после rotation
SharedFlow(replay=0) без текущего значения эмиссия «в пустоту», если нет подписчика
Channel + receiveAsFlow ровно один consumer, буфер нужен активный коллектор в нужный момент
эффект как state нет отдельного канала не всё выражается состоянием
  1. Почему не StateFlow — она конфлейтит и «запоминает»; для «сделай ровно один раз» память — это баг, а не фича.
  2. Channel — прагматичный выборChannel(Channel.BUFFERED).receiveAsFlow(): событие получит ровно один коллектор и не потеряется при пересоздании UI (буфер придержит).
  3. По возможности — state — «показать ошибку» часто честнее как UiState.Error, а не как разовый выстрел.

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

06

Repository как single source of truth: как выглядит offline-first репозиторий?

Короткий ответ: Репозиторий прячет источники (сеть, БД, кэш) за единым API и назначает один источник правды — обычно локальную БД. UI всегда читает из БД (через Flow), а сеть лишь пишет свежие данные в БД. Экран показывает данные мгновенно и никогда не «мигает» между источниками.

Подробно:

class UserRepository(private val dao: UserDao, private val api: Api) {
    // UI подписан ТОЛЬКО на БД — это единственный источник правды
    fun observeUser(id: String): Flow<User> = dao.observe(id)

    // сеть не отдаётся в UI напрямую — она обновляет БД
    suspend fun refresh(id: String) {
        val fresh = api.getUser(id)      // может упасть — БД остаётся валидной
        dao.upsert(fresh.toEntity())     // Flow из observe() эмитит сам
    }
}
  1. Один источник правды — UI знает только про БД; появилась связь — обновили БД, Flow протолкнул новое значение на экран.
  2. Offline-first — без сети экран всё равно показывает последнее закэшированное; сбой refresh() не ломает отображение.
  3. Граница абстракции — ViewModel не знает, откуда данные; поменять Retrofit на другой источник можно, не трогая presentation.

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

07

Clean Architecture на Android: какие слои, что такое правило зависимостей и когда UseCase — лишняя церемония?

Короткий ответ: Три слоя — presentation (ViewModel + UI), domain (сущности и UseCase, чистый Kotlin без Android) и data (репозитории, DTO, Room, Retrofit). Правило зависимостей: стрелки направлены внутрь, к domain. Domain ни о ком не знает; data и presentation зависят от domain, а не наоборот. UseCase оправдан при реальной логике или переиспользовании; тонкая обёртка над repo.getX() — церемония.

Подробно:

  presentation  ───►  domain  ◄───  data
  (ViewModel/UI)     (entities,     (repo impl, DTO,
                      use cases,     Room, Retrofit)
                      repo интерфейсы)
            зависимости всегда указывают ВНУТРЬ
  1. Правило зависимостей — интерфейсы репозиториев живут в domain, реализации — в data; presentation зовёт domain. Внешние слои знают о внутренних, не наоборот.
  2. Три модели, не одна — DTO (форма бэка), domain-модель (язык бизнеса), UI-модель (что рисуем). У каждой своя причина меняться: смена поля в API не должна ломать Composable.
  3. Когда UseCase — ценность — есть логика (комбинирование источников, правила, валидация) или её переиспользуют несколько ViewModel. Когда class GetUser(repo) { operator fun invoke() = repo.getUser() } — это лишний слой ради слоя.

⚠️ Частая ошибка: domain, который импортирует android.*, Context или Retrofit-аннотации. Тогда правило зависимостей нарушено — domain обязан компилироваться как чистый Kotlin-модуль, без Android SDK.

08

Hilt: как связаны компоненты и скоупы, чем @Binds отличается от @Provides и когда стоит Koin?

Короткий ответ: Каждый компонент Hilt соответствует своему скоупу и времени жизни. @Binds связывает интерфейс с его реализацией (для классов с constructor-injection — генерит меньше кода), @Provides описывает, как создать объект (для чужих классов и билдеров). Без скоупа Hilt отдаёт новый инстанс на каждый запрос. Koin строит граф в рантайме через DSL — быстрее сборка, но ошибки графа видны только при запуске; Hilt проверяет граф в компайл-тайме.

Подробно:

Компонент Скоуп Живёт
SingletonComponent @Singleton всё приложение
ActivityRetainedComponent @ActivityRetainedScoped переживает пересоздание Activity
ViewModelComponent @ViewModelScoped время жизни ViewModel
ActivityComponent @ActivityScoped одну Activity
  1. Скоуп = кэш в компоненте@Singleton-биндинг создаётся один раз на приложение; unscoped-биндинг создаётся заново при каждой инъекции.
  2. @Binds против @Provides@Binds — абстрактный метод в абстрактном @Module, только «интерфейс → реализация»; @Provides — обычная функция с телом, когда объект надо собрать (Retrofit, Room, чужой класс).
  3. Hilt vs Koin — компайл-тайм безопасность и производительность против скорости сборки и гибкости; Koin к тому же работает в Kotlin Multiplatform, Hilt — нет.

⚠️ Частая ошибка: ждать, что зависимость «сама» будет синглтоном, потому что её инжектят везде. Без @Singleton на биндинге Hilt создаст новый объект на каждую точку инъекции — общий кэш/OkHttpClient незаметно размножится.

09

Многомодульность: делить по фичам или по слоям, и зачем различать api и implementation?

Короткий ответ: Деление по фичам (feature:login, feature:cart) масштабируется лучше чистого деления по слоям: границы совпадают с продуктом, команды не толкаются в одних файлах. На практике — гибрид: фичи, а внутри каждой слои (data/domain/ui). implementation прячет транзитивную зависимость, api протаскивает её наружу — это напрямую влияет на скорость сборки.

Подробно:

Стратегия Плюс Минус
По слоям просто на старте один :feature-монолит, слабый параллелизм
По фичам изоляция, параллельная сборка нужен слой core/common
Гибрид (фича × слой) чёткие границы + переиспользование больше модулей и Gradle-конфигурации
  1. Зачем вообще модули — Gradle пересобирает только изменённые модули и собирает независимые параллельно; на больших проектах это минуты против десятков минут.
  2. api vs implementationimplementation НЕ протекает наружу: правка внутренностей не форсит пересборку зависящих модулей. api делает зависимость частью публичного контракта модуля.
  3. Правило по умолчаниюimplementation; api — только когда тип реально виден в публичном API модуля.

⚠️ Частая ошибка: ставить api везде «на всякий случай». Тогда любая правка каскадом инвалидирует пол-графа зависимостей, и весь выигрыш многомодульности в скорости сборки испаряется.

10

«Расскажите про вашу архитектуру» — спроектируйте экран профиля от API до UI.

Короткий ответ: Снизу вверх: Retrofit отдаёт ProfileDto → маппер → domain-модель ProfileRepository (single source of truth в Room) → опциональный UseCase → ViewModel собирает ProfileUiState → single-activity + Navigation показывает Composable/Fragment, подписанный на StateFlow; действия пользователя идут интентами вверх, навигация — одноразовым событием.

Подробно:

 API ─► ProfileDto ─[map]─► Profile ─► Repository ─► (UseCase) ─► ViewModel
(Retrofit)  (data)          (domain)   (Room = SSOT)             │ ProfileUiState

                                             UI (Composable) ◄── StateFlow (state вниз)
                                                     └── intents/navEvent ── вверх
  1. DataApi + Dao; репозиторий пишет сеть в Room и отдаёт наверх Flow<Profile> из БД. Три модели (DTO/domain/UI) изолируют изменения формата.
  2. DomainProfile и UseCase, если есть логика; чистый Kotlin, зависимостей на Android нет.
  3. Presentation — ViewModel держит StateFlow<ProfileUiState> (sealed), одноразовые эффекты — через Channel; UI — чистая функция от состояния.
  4. Каркас и связка — одна Activity + Navigation с типобезопасными аргументами и deep links; граф собирает Hilt.

⚠️ Частая ошибка: тащить ProfileDto прямо в Composable, экономя на маппинге. Любое переименование поля на бэке ломает UI и тесты; три модели существуют именно затем, чтобы у каждого слоя была одна причина меняться.

Источники

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

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

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

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

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

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

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

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

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

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

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

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

RSS