
Думаю, многие из нас успели заметить стремительное изменение рынка в последние пару лет: он плавно превратился из рынка соискателя в рынок нанимателя и, пожалуй, стал наиболее конкурентным и жёстким за всё время существования 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/