Топ-10 вопросов с Android-собеседований в 2026 — с разборами

от автора

Думаю, многие из нас успели заметить стремительное изменение рынка в последние пару лет: он плавно превратился из рынка соискателя в рынок нанимателя и, пожалуй, стал наиболее конкурентным и жёстким за всё время существования IT в России.

К тому же, старая проблема поиска работы тоже никуда не делась: умение писать классные Android-приложения не всегда гарантирует успешный найм. То, что спрашивают на интервью, и задачи, которые мы решаем каждый день, совпадают далеко не всегда.

Поэтому навык прохождения собеседований становится всё важнее. В этой небольшой статье хотел бы разобрать 10 популярных вопросов, с которыми можно столкнуться на собеседованиях по Android в 2026 (Kotlin, Корутины и Flow, Android, Jetpack Compose), поехали 🚀

Kotlin

1. Что такое data class и чем он отличается от обычного?

Это специальная версия класса, для которой компилятор по полям первичного конструктора генерирует набор «плюшек»:

  • equals() и hashCode() — по полям конструктора. Реализации по умолчанию (из Any) сравнивают по ссылке и бесполезны, а так класс сразу готов к использованию в хеш-коллекциях (HashMap, HashSet).

  • toString() — выводит свойства первичного конструктора, удобно для логов и отладки.

  • copy() — создаёт изменённую копию объекта, меняя только нужные поля; это поддерживает работу в иммутабельном стиле. Но иммутабельность Kotlin не навязывает (в конструкторе допустим var), а сам copy() поверхностный (копирует ссылочные поля по ссылке, а не клонирует их).

  • componentN() — доступ к полям по позиции, что открывает destructuring (val (name, age) = user).

data class User(val name: String, val age: Int)val user = User("John Doe", 56)val older = user.copy(age = 57)   // меняем только ageval (name, age) = older           // destructuringprintln(user)   // User(name=John Doe, age=56)

Важная деталь: data class требует хотя бы одного поля var или val в первичном конструкторе (обычному классу это не нужно) — иначе генерировать особо нечего.

2. Есть ли у data class недостатки? Можно ли просто всё объявлять как data?

Можно, но не нужно.

Во-первых, кодогенерация. equals/hashCode/toString/copy/componentN создаются для каждого data-класса, и на большом проекте это заметно влияет на размер приложения.

Во-вторых (и, пожалуй, важнее размера), весь этот набор осмыслен только для реальных контейнеров данных (POJO, DTO). Если навесить data на обычный класс — есть вероятность получить неожиданное поведение:

  • equals/hashCode сравнивают по значению, по всем полям конструктора. Объектам-значениям (деньги, координаты) это идеально, а вот, например, сущностям БД — уже нет: две версии одного пользователя (поменялся возраст) по значению окажутся «разными», и ломаются дедупликация и поиск в HashSet/HashMap. Для таких сущностей корректнее сравнение по id или по ссылке.

  • toString() печатает все поля конструктора — и какой-нибудь токен или пароль незаметно утекает в логи.

  • Исторический нюанс: copy() генерировался публичным даже при приватном первичном конструкторе, из-за чего создание объекта нельзя было полностью спрятать за фабрикой. В Kotlin 2.0.20 это поведение изменилось: видимость copy() постепенно приводят к видимости конструктора (через аннотации @ConsistentCopyVisibility/@ExposedCopyVisibility).

То есть data class — специализированный инструмент для классов-данных, а не универсальная улучшенная версия обычного class. И в Kotlin он неслучайно не включён по умолчанию. Тот же принцип разделения виден и в других языках: например, в Swift struct (тип-значение) отделён от class (ссылочный тип, идентичность по ссылке).

3. Что такое sealed interface и чем он отличается от sealed class?

sealed interface (появился в Kotlin 1.5) даёт те же преимущества, что и sealed class — фиксированный на этапе компиляции список наследников и исчерпывающий when без else, — но это интерфейс. А значит, допускает множественную реализацию: один тип может входить сразу в несколько sealed-иерархий.

Критерии выбора тут отчасти похожи на «абстрактный класс vs интерфейс»: нужно общее состояние и реализация — класс; нужна гибкость и множественное наследование — интерфейс.

Классический пример (его хорошо разбирает Хорхе Кастильо): у разных операций (логин, загрузка пользователя) есть общие ошибки. С sealed class их не сделать членами сразу обеих иерархий (одно наследование). С sealed interface — легко:

sealed interface LoginErrorsealed interface GetUserError// общие ошибки — сразу в ОБЕИХ иерархиях (так умеет только interface):sealed class CommonError : LoginError, GetUserErrorobject ServerError : CommonError()object Unauthorized : CommonError()// специфичные:object WrongPassword : LoginErrorobject UserNotFound : GetUserError

Coroutines и Flow

4. Достаточно ли модификатора suspend, чтобы функцию можно было безопасно вызывать с UI-потока?

Нет. suspend не гарантирует, что функция не выполнит долгую или блокирующую операцию. Простой контрпример — чтение файла:

suspend fun readContent(path: String): String {    return File(path).readText() // блокирующее IO!}

Вызовем такое из корутины на Main-диспатчере — file IO произойдёт прямо на UI-потоке. Чинится переключением на нужный диспатчер через withContext:

suspend fun readContent(path: String): String = withContext(Dispatchers.IO) {    File(path).readText()}

В корутинах есть рекомендуемый контракт Main Safety: все suspend-функции должны быть безопасны для вызова с UI-потока (подробнее — в посте одного из авторов корутин «Blocking threads, suspending coroutines»). suspend — это лишь про возможность приостановки, а не про то, на каком потоке выполнится тело.

5. Как сменить диспатчер, на котором Flow эмитит значения? Сработает ли что-то вроде этого?

flow {    withContext(Dispatchers.IO) {        emit(...)    }}

Нет — этот код упадёт с IllegalStateException: emit нельзя вызывать из другого контекста, чем тот, где запущен flow. Правильный способ — оператор flowOn, который меняет контекст выполнения вверх по цепочке:

flow {    emit(readFromDb())     // выполнится на IO}    .map { it.toUi() }     // тоже на IO: flowOn действует вверх по цепочке    .flowOn(Dispatchers.IO)    .collect { render(it) } // а это уже в контексте коллектора

О причинах такого дизайна — пост «Execution context of Kotlin Flows».


Android

6. Чем отличается Application Context от Activity Context?

Два практически важных следствия.

Утечки памяти. Ссылку на activity context нельзя надолго отдавать в то, что переживает саму Activity (синглтоны, статические поля). Классика — ViewModel: при смене конфигурации Activity пересоздаётся, а ViewModel живёт дальше, так что ссылка из неё на activity context превращается в утечку. В таких местах используют application context (а UI-объекты в ViewModel не хранят вовсе).

Темы и UI. У application context нет темы конкретной Activity (это не ContextThemeWrapper) — он несёт лишь базовую тему приложения. Поэтому app context не годится для UI, где важны тема экрана, окно или inflater: Material-компоненты упадут с ошибкой про Theme.AppCompat, а AlertDialog — с BadTokenException, потому что диалогу нужно окно Activity. Подробный разбор, какой контекст для чего годится, есть на Stack Overflow.

А что в Compose? Сам класс Context никуда не делся: внутри composable он доступен через LocalContext.current, и это, как правило, тот же activity context. Но вот часть «вьюшных» граблей отваливается. Тему в Compose задаёт не Context.getTheme(), а composition locals — MaterialTheme или (что в реальных приложениях не редкость) собственная кастомная тема, не завязанная на Material, так что проблема «не та тема» просто не возникает. А диалоги рисуются composable-функциями, которым вручную передавать activity context, как старому android.app.AlertDialog, уже не надо. Ну и, наконец, правило про утечки остаётся в силе: LocalContext.current (как и любой activity context) нельзя сохранять в долгоживущем объекте (ViewModel и т.п.), а для app-scoped вещей по-прежнему используют applicationContext.

7. По умолчанию ViewModel создаётся через пустой конструктор. Как передать в него зависимости?

Дефолтная фабрика умеет создавать только ViewModel с пустым конструктором (плюс варианты с Application и SavedStateHandle). Как только появляются свои зависимости — нужна ViewModelProvider.Factory. Без DI это выглядеть может, например, так — через DSL viewModelFactory:

class UserViewModel(private val repo: UserRepository) : ViewModel() {    companion object {        fun factory(repo: UserRepository) = viewModelFactory {            initializer { UserViewModel(repo) }        }    }}val vm: UserViewModel by viewModels { UserViewModel.factory(repo) }

На практике фабрику чаще не пишут руками, а полагаются на DI. С популярным вариантом Hilt вью-модель помечают как @HiltViewModel с @Inject-конструктором, а саму фабрику библиотека генерирует за нас:

@Singletonclass UserRepository @Inject constructor() {    suspend fun loadName(): String { /* загрузка из сети/БД */ }}@HiltViewModelclass UserViewModel @Inject constructor(    private val repo: UserRepository,) : ViewModel() { /* ... */ }@Composablefun UserScreen(viewModel: UserViewModel = hiltViewModel()) { /* ... */ }

Jetpack Compose

8. Какие фазы проходит Compose, чтобы отрисовать кадр?

  • Composition (что показывать). Compose выполняет composable-функции и собирает дерево UI-элементов. Структура — есть, размеров и пикселей — ещё нет.

  • Layout (где разместить). Каждый элемент получает ограничения по размеру, измеряет вложенные и выбирает их позиции.

  • Drawing (как нарисовать). Уже измеренные и размещённые элементы отрисовываются на Canvas.

Важно для производительности: Compose отслеживает, в какой фазе читается состояние. Если состояние читается только в Layout/Drawing (например, через Modifier.offset { ... }), его изменение может пропустить Composition и перезапустить лишь нужную фазу.

Официальный разбор всех трёх фаз — в документации и видео «From data to UI: Compose phases» из серии MAD Skills.

9. Как Compose отслеживает состояние и понимает, какие участки UI перевыполнять?

Через snapshot-систему. Отслеживаемое состояние — это наблюдаемые объекты State/MutableState (их создаёт mutableStateOf), а не обычные переменные. Пока composable во время композиции читает такой State (через .value или делегат by), snapshot-система записывает это чтение и запоминает, какой участок UI (recompose scope) от какого состояния зависит.

Когда State меняется, перевыполняются только те участки, что его читали (и их дочерние composable, которые нельзя пропустить), а остальное не трогается — точечно, без перерисовки всего экрана:

@Composablefun Counter() {    var count by remember { mutableStateOf(0) }    Title()              // count не читает → при изменении count пропускается    Text("Счёт: $count") // читает count → рекомпозится}

Ключевой момент: отслеживается только Compose-состояние (mutableStateOf и производные). Обычный var Compose не видит — его изменение рекомпозицию не запустит. Поэтому состояние и держат в mutableStateOf.

Официально это описано в гайде Lifecycle of composables in Compose (раздел про smart recomposition).

10. Чем remember отличается от rememberSaveable?

remember хранит значение только в пределах композиции: оно переживает рекомпозиции, но теряется при пересоздании активити (поворот экрана, смена темы, убийство процесса системой).

rememberSaveable дополнительно сохраняет значение в Bundle (как onSaveInstanceState у View), поэтому оно переживает и смену конфигурации, и восстановление процесса. В него кладут то, что сериализуется в Bundle автоматически (примитивы, Parcelable); для своих типов есть listSaver/mapSaver.

Грубое правило: некритичное UI-состояние (идёт ли анимация, развёрнут ли элемент) — remember; то, что обидно потерять при повороте (введённый текст, позиция списка) — rememberSaveable.


На этом все 😊 Надеюсь, было полезно. Если вам вдруг понравился такой формат и сейчас актуальна тема собеседований, то приглашаю в мой небольшой бесплатный курс «Топ-25 вопросов на Android-собеседовании в 2026» — там ещё 15 вопросов по тем же темам плюс пара практических задач с разбором: https://stepik.org/course/292731

ссылка на оригинал статьи https://habr.com/ru/articles/1053262/