От соискателя на должность разработчика Android требуются знания Android, Kotlin и другие навыки. Хотя невозможно предугадать, какие вопросы окажутся на собеседовании, подготовиться к нему, овладев необходимыми для этой работы фундаментальными знаниями, вполне реально.
Нужно ознакомиться с минимальными квалификационными требованиями и предпочтениями работодателя для этой должности. Вопросы собеседования сильно варьируются в зависимости от команды, сферы деятельности компании, ее культуры разработки. Поэтому подготовка затачивается под конкретную команду или компанию.
Тем не менее для должностей разработчиков Android имеются общие вопросы. Девять из них представлены в Dove Letter — репозитории, где изучают, обсуждают и делятся идеями об Android и Kotlin, вопросами собеседований, рекомендациями относительно кода, статьями, актуальными новостями.
Android
Более чем десятилетняя история Android — уже анонсирован Android 15 — отмечена значительными техническими достижениями. Несмотря на это, основные системы и компоненты — жизненные циклы Activity и Fragment или Intents — в основном остались неизменными. Понимать эти фундаментальные системы, даже если они кажутся олдскульными, по-прежнему важно для любого разработчика Android.
1. Опишите жизненный цикл Activity
В Android это различные состояния в течение жизненного цикла activity — от создания до уничтожения. Для эффективного управления ресурсами, обработки пользовательского ввода и обеспечения бесперебойного пользовательского взаимодействия важно понимать эти состояния. Вот основные этапы жизненного цикла Activity:

- onCreate(): при создании activity этот метод вызывается первым. Здесь инициализируется activity, настраиваются компоненты пользовательского интерфейса, восстанавливается любое сохраненное состояние экземпляра. За жизненный цикл activity метод вызывается лишь раз, пока не уничтожен и не создан заново.
- onStart(): activity становится видимым для пользователя, но еще не интерактивным. Метод вызывается после onCreate() и до onResume().
- onRestart(): если activity остановлен, а затем перезапущен, например при возвращении к нему пользователя, этот метод вызывается до onStart().
- onResume(): activity в приоритете и пользователь взаимодействует с ним. Здесь возобновляются любые приостановленные обновления ПИ, анимации или прослушиватели ввода.
- onPause(): вызывается, когда activity частично скрыт другим activity, например диалогом. Activity остается видимым, но не в фокусе. Им обычно приостанавливаются анимация, обновления сенсоров или сохранение данных.
- onStop(): activity больше не виден пользователю, например, когда приоритетным становится другой activity. Пока activity остановлен, ненужные ресурсы — фоновые задачи или тяжелые объекты — следует высвободить.
- onDestroy(): вызывается до того, как activity полностью уничтожится и удалится из памяти. Это финальный метод очистки для высвобождения всех ресурсов.
Резюмируем
Activity пропускается по этим методам на основе пользовательских взаимодействий и управления ресурсами приложения в системе Android. С этими обратными вызовами разработчики управляют переходами, сохраняют ресурсы и обеспечивают бесперебойную работу пользователей. Подробнее — в официальной документации Android.
Попадаются и аналогичные вопросы, например о жизненном цикле
FragmentилиViewв Android.
2. Что такое Intent?
В Android это абстрактное описание выполняемой операции, объект обмена сообщениями для взаимодействия между разными activity, службами и широковещательными приемниками. Ими также передаются данные между компонентами, так что intent становится фундаментальной частью компонентной архитектуры Android.
В Android имеется две основные разновидности intent: явные и неявные.
1. Явное intent
- Определение: явным intent указывается точный компонент, то есть activity или служба, вызываемые прямым его именованием.
- Вариант применения: явные intent используются, когда известен целевой компонент, например, запускается конкретный activity в приложении.
- Пример:
val intent = Intent(this, TargetActivity::class.java)
startActivity(intent)
- Сценарий: если переключиться с одного activity на другой в том же приложении, используется явное intent.
2. Неявное intent
- Определение: неявным intent не указывается конкретный компонент, а объявляется выполняемое общее действие. На основе действия, категории и данных системой определяется, какими компонентами обрабатывается intent.
- Вариант применения: неявные intent используются при выполнении действия, которое обрабатывается другими приложениями или системными компонентами, например открытие URL-адреса или совместное использование контента.
- Пример:
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.example.com")
startActivity(intent)
- Сценарий: если открыть веб-страницу в браузере или поделиться контентом с другими приложениями, используется неявное intent. Системой определяется, каким приложением обработать этот intent.
Резюмируем
Явные intent используются для внутренней навигации по приложению, где целевой компонент известен. Неявные intent применяются для действий, обрабатываемых внешними приложениями или другими компонентами без прямого указания цели. Благодаря этому экосистема Android гибче и взаимодействие приложений бесперебойное.
3. В чем разница между Serializable и Parcelable?
В Android Serializable и Parcelable — это механизмы передачи данных между компонентами вроде activity или fragment, отличаются друг от друга производительностью и реализацией. Сравним их:
Serializable
- Это стандартный интерфейс Java для преобразования объекта в поток байтов, который затем передается между activity или записывается на диск.
- Работа
Serializableоснована на рефлексии Java: чтобы сериализовать объект, во время выполнения системой динамически проверяется класс и его поля. - Производительность:
SerializableмедленнееParcelable, поскольку рефлексия — процесс небыстрый. К тому же при сериализации генерируется много временных объектов, отчего увеличивается расход памяти. - Вариант применения: в сценариях, где производительность не важна, или при работе с кодовыми базами без специфики Android.
Parcelable
- Это Android-интерфейс, предназначенный специально для высокопроизводительного межпроцессного взаимодействия в компонентах Android.
- Производительность:
ParcelableбыстрееSerializable, поскольку оптимизирован для Android и не полагается на рефлексию. Многочисленные временные объекты им не создаются, поэтому сборка мусора минимизируется. - Вариант применения:
Parcelableпредпочтителен в сценариях передачи данных на Android, где важна производительность, особенно при межпроцессном взаимодействии или передаче данных между activity или службами.
Резюмируем
В целом Parcelable — рекомендуемый подход для приложений Android, в большинстве случаев у него выше производительность. Однако, если нужна простота, а производительность не так важна, проще реализовать Serializable.
Serializableиспользуется для случаев попроще: в операциях, где производительность не важна, или при работе с кодом без специфики Android.Parcelableиспользуется при работе с компонентами Android, где важна производительность: для механизма межпроцессного взаимодействия Android он намного эффективнее.
Kotlin
На Google I/O 2019 объявили о применении при разработке Android подхода Kotlin-first, после чего востребованность Kotlin резко повысилась. К концу 2024 года большинство проектов на Android перевели на Kotlin, особенно после выхода стабильной версии Jetpack Compose, которая стала очень популярной. Так что сегодня во многих командах в большинстве случаев вместо Java используют Kotlin.
1. Что такое «класс данных» в Kotlin и чем он отличается от обычного класса?
В Kotlin класс данных — это особая разновидность классов, специально предназначенная для хранения данных. Для классов данных здесь автоматически генерируются методы, эти классы идеальны для представления простых объектов, в которых содержатся данные.
Основные характеристики классов данных
Когда объявляется класс данных, в Kotlin автоматически генерируются:
equals(): которым сравнивается два экземпляра класса на предмет соответствия их свойств.hashCode(): которым на основе свойств генерируется хеш-код.toString(): выдается строковое представление объекта со значениями его свойств.copy(): создается объект со свойствами, скопированными из уже имеющегося объекта, и с возможностью изменения конкретных значений.- Функции компонентов: для объявлений деструктурирования, например
component1(),component2(), благодаря чему легко извлекаются свойства.
Пример
В этом примере классу User в Kotlin автоматически предоставляются equals(), hashCode(), toString() и copy():
data class User(val name: String, val age: Int)
Различия между классом данных и обычным классом
- Сокращение стереотипного кода: в обычном классе
equals(),hashCode(),toString()и другие вспомогательные методы переопределяются вручную. С классом данных Kotlin они генерируются автоматически. - Требование по основному конструктору: классом данных требуется объявление минимум одного свойства в основном конструкторе, обычному классу этого не требуется.
- Вариант применения: в классах данных в основном хранятся неизменяемые данные, хотя могут применяться изменяемые свойства, обычные же классы используются для любого поведения или логики.
Пример обычного класса для сравнения
В этом примере классу User в Kotlin автоматически предоставляются equals(), hashCode(), toString() и copy():
class Person(val name: String, val age: Int)
Резюмируем
Классы данных используются для объектов, в которых только содержатся данные, в Kotlin автоматически генерируются вспомогательные методы equals(), hashCode(), toString(), copy(). Обычный класс гибче, но эти методы по умолчанию им не предоставляются, поэтому он скорее для объектов с поведением и сложной логикой.
2. Что такое «расширение», каковы его достоинства и недостатки?
Расширения — это способ добавить функциональности имеющимся классам без изменения их кода напрямую. В Kotlin класс «расширяется» новыми функциями или свойствами с помощью функций-расширений и свойств-расширений, особенно при совершенствовании классов из сторонних библиотек или стандартной библиотеки, где нет доступа к исходному коду.
Пример функции-расширения
Добавим в класс Int функцию isEven():
fun Int.isEven(): Boolean {
return this % 2 == 0
}
val number = 4
println(number.isEven()) // Вывод: true
Здесь isEven() становится новой функцией, доступной для всех объектов Int, даже если сам класс Int не изменили.
Пример свойства-расширения
В Kotlin к классу аналогично добавляются и новые свойства, но ими не сохраняется состояние, это всего лишь синтаксический сахар для функций-геттеров:
val String.firstChar: Char
get() = this[0]
val text = "Hello"
println(text.firstChar) // Вывод: H
Другой пример — добавление свойства-расширения к имеющемуся типу:
val String.Companion.Empty: String
get() = ""
// Использование
val fakeUser = User.createUser(name = String.Empty) // instead of User.createUser(name = "")
Преимущества расширений
- Выше удобство восприятия: с расширениями код удобнее для восприятия и выразительнее.
- Модульность: с ними функциональность добавляется без изменения исходного класса.
- Переиспользуемость кода: расширения переиспользуются в различных частях приложения, благодаря чему избегается стереотипный код.
Несмотря на мощь и гибкость расширений Kotlin, у них имеются недостатки и ограничения.
Недостатки расширений
- Возможность путаницы: расширения иногда чреваты путаницей, особенно если в классе уже имеются функции или расширения с похожими названиями. Если у функции-расширения и функции-члена одинаковое название, приоритет отдается функции-члену, что может быть неочевидным.
- Злоупотребление расширениями чревато плохой организацией кода: если при добавлении функций к имеющимся классам переусердствовать с расширениями, усложняются перемещение по коду и его сопровождение, особенно когда эти функции разбросаны по файлам или модулям. Это чревато «раздутым» API и менее целостной кодовой базой.
- Сложно отследить происхождение функций: в больших кодовых базах трудно выявить, где определена функция-расширение, она может находиться в другом модуле или пакете. Из-за этого усложняются навигация по коду и его отладка.
Резюмируем
Расширения в Kotlin — это мощные инструменты, которыми в условиях модульности четко совершенствуется функциональность, для чего не требуется наследования или изменения исходного класса. Расширения Kotlin — это удобство и гибкость, но применять их нужно с умом, избегая усложнений и поддерживая четкий, сопровождаемый код.
3. В чем разница между корутинами и потоками?
Разница между корутинами Kotlin и потоками в Android — и в Kotlin в целом — заключается в том, как в них управляют конкурентностью, потреблением ресурсов и производительностью.
- Легковесные против тяжеловесных: корутины легковесны. Они выполняются в одном потоке, но приостанавливаются без блокировки потока. Благодаря этому тысячи корутин выполняются конкурентно в меньшем количестве потоков с минимальными накладными расходами. Потоки, напротив, тяжеловесны. У каждого из них имеются собственные память и ресурсы, переключение между потоками сопряжено с бо́льшими накладными расходами, что чревато большим расходованием ресурсов при работе многочисленными потоками.
- Конкурентность против параллелизма: корутинами обеспечивается конкурентное выполнение задач, которые приостанавливаются и возобновляются без занятия отдельного потока. Задачи запускаются ими не обязательно параллельно, но поддерживается кооперативная многозадачность. Потоками обеспечивается параллелизм, при этом задачи запускаются одновременно на нескольких ядрах. Задачи выполняются каждым потоком независимо, что приходится кстати для операций, скорость выполнения которых ограничена быстродействием процессора.
- Блокировка против приостановки потоков: корутинами используется механизм приостановки, то есть в ожидании завершения задачи поток ими не блокируется. Когда корутина приостанавливается, например, в ожидании сетевого ответа, в основном потоке выполняются другие корутины. Потоками выполняются блокирующие операции. Если поток находится в ожидании операции ввода-вывода или вызова sleep, другие задачи им не выполнятся.
- Эффективность: корутинами эффективнее используются память и процессор, поскольку не приходится переключать контекст между потоками, и потребляется меньше системных ресурсов. Потоками потребляется больше ресурсов из-за накладных расходов на создание потоков, планирование и переключение контекста между потоками.
- Переключение контекста: корутины переключаются между задачами в точках приостановки, например,
delay()илиwithContext(), а это дешевле переключения между потоками. Между потоками же контекст переключается операционной системой, что дороже по производительности. - Варианты применения: корутины идеальны для задач с ограничением скорости ввода-вывода: выполнение сетевых запросов, обработка операций базы данных, обновления ПИ. Потоки же оптимальнее для задач, скорость выполнения которых ограничена быстродействием процессора, где действительно требуются параллельные вычисления — например, при интенсивной обработке изображений или больших объемах вычислений.
- Обработка ошибок: API-интерфейсами структурированной конкурентности, такими как
JobилиCoroutineExceptionHandler, в корутинах беспроблемно обрабатываются исключения и отменяются задачи, а конструктором корутин —launchилиasync— мгновенно распространяются исключения. Для отмены задач и распространения исключений потокам требуется больше ручной обработки ошибок — try-catch илиuncaughtExceptionHandler— и координации.
Резюмируем
Корутины оптимальнее для управления многочисленными задачами с конкурентным выполнением и минимальными накладными расходами, потоки же предназначены скорее для параллельного выполнения, когда требуется несколько процессорных ядер.
Попадаются и аналогичные вопросы, например, об изолированных/вспомогательных классах или Flow, StateFlow и SharedFlow.
Jetpack Compose
Jetpack Compose — современный инструментарий ПИ, которым с момента выхода стабильной версии 1.0 продемонстрирован огромный потенциал. Применение этого инструмента на продакшене резко увеличилось: сейчас в Google Play Store доступно более 125 000 приложений, созданных на Jetpack Compose.
Однако во многих компаниях еще продолжается процесс освоения Jetpack Compose или рассматривается возможность миграции из традиционных систем представлений, ведь перевести на него крупномасштабный проект целиком — дорогое удовольствие. Будут ли вопросы о Jetpack Compose на собеседовании, во многом зависит от конкретной компании.
1. Что такое «рекомпозиция»?
В Jetpack Compose это процесс, при котором фреймворком перерисовываются части пользовательского интерфейса с учетом обновленных данных или состояния. То eсть не перерисовывается весь экран, а по-умному «перекомпоновываются» — это и есть интеллектуальная рекомпозиция — только те части ПИ, которые подлежат изменению. Поэтому Jetpack Compose эффективнее традиционных фреймворков ПИ.

Принцип работы рекомпозиции
- ПИ, управляемый состоянием: Compose — это декларативный фреймворк пользовательского интерфейса, создаваемого на основе текущего состояния. Когда изменяется состояние, в Compose активируется рекомпозиция соответствующих частей дерева ПИ.
- Выборочная перерисовка: перекомпонуются только те функции
composable, которыми обновленное состояние используется. Если функция не зависит от измененного состояния, она не перекомпоновывается. Поэтому такое обновление ПИ эффективнее. - Функции
composable: рекомпозиция выполняется на функциональном уровне, при этом в Compose повторно вызываются соответствующие функцииcomposableс новыми данными. Чтобы избежать лишних перерисовок, Compose по максимуму переиспользуется из предыдущей композиции.
Например, в тексте отображается счетчик нажатий и кнопка, при каждом нажатии на которую этот счетчик увеличивается:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increase")
}
}
}
В этом примере:
- При каждом нажатии кнопки обновляется
countи активируется рекомпозиция функцииCounter. - В Compose перерисовывается только компонуемый
Text, из composable и отображается значение счетчика, а не весь ПИ.
Подводя итог, отметим ключевые моменты рекомпозиции:
- Основывается на состоянии: рекомпозиция выполняется при изменении состояния, которое обычно содержится в
rememberиmutableStateOf. - Оптимизированная производительность: в Compose перекомпонуется только то, что в этом нуждается, поэтому производительность повышается.
- Идемпотентность: на одинаковый ввод функциями
composableвыдается одинаковый вывод ПИ, благодаря чему рекомпозиция надежна.
Вот функции библиотеки Jetpack Compose Runtime, тесно связанные с управлением состоянием, которыми сохраняются данные при рекомпозициях или эффективно обрабатываются побочные эффекты:
remember: при выполнении рекомпозиций значения кэшируются и потому не сбрасываются всякий раз.derivedStateOf: рекомпозиция оптимизируется, активируясь только при изменении полученного состояния.LaunchedEffect,SideEffect,DisposableEffect: побочные эффекты при выполнении рекомпозиций контролируются в функцияхcomposable.
Резюмируем
Рекомпозиция — это процесс, при котором элементы ПИ обновляются и перерисовываются на основе новых состояний, но только те части ПИ, которые подлежат изменению. Этим подходом, называемым «интеллектуальной рекомпозицией», в Jetpack Compose эффективно обновляется пользовательский интерфейс, адаптивность которого сохраняется благодаря синхронизации с текущим состоянием.
2. Что такое «поднятие состояния»?
В Jetpack Compose это шаблон проектирования, при котором состояние «поднимается» к вызывающему или родительскому composable, благодаря чему оно контролируется родителем, а дочерний composable фокусируется только на отображении ПИ. Эта концепция аналогична подходу React к управлению состоянием. Основная цель поднятия состояния — разделение обязанностей, при котором компоненты ПИ остаются без состояния, повышается переиспользуемость, упрощается тестирование.
При поднятии состояния:
- Состояние управляется в родительском
composable. - События или триггеры вроде
onClickилиonValueChangeпередаются от потомка обратно родителю, и состояние обновляется. - Затем обновленное состояние снова передается в параметрах потомку, чем создается однонаправленный поток данных.
Пример:
@Composable
fun Parent() {
var sliderValue by remember { mutableStateOf(0f) }
SliderComponent(
value = sliderValue,
onValueChange = { sliderValue = it }
)
}
@Composable
fun SliderComponent(value: Float, onValueChange: (Float) -> Unit) {
Slider(value = value, onValueChange = onValueChange)
}
Здесь в composable Parent контролируется состояние sliderValue, а в SliderComponent нет состояния: от родителя им получаются значение и обработчик события. При таком подходе в приложениях Compose совершенствуются структура и сопровождаемость.
Вот преимущества поднятия состояния в Jetpack Compose:
- Единый источник истины: поднятием состояния обеспечивается, что состояние управляется в одном месте — обычно в родительском
composable, предотвращаются конфликты состояний дочерних и родительскихcomposable. Этим повышается согласованность данных в приложении. - Переиспользуемость: дочерними
composableсобственные состояния не управляются, поэтому они переиспользуются в разных частях приложения. Как сделать компоненты универсальными и переиспользуемыми? Передавая состояния и обработчики событий. - Разделение обязанностей: при поднимании состояния к родителю дочерние
composableостаются без состояния, фокусируясь исключительно на отображении ПИ. Так компоненты упрощаются, становятся удобнее для восприятия и сопровождения. - Повышенная тестопригодность: эти
composableбез состояния легче тестировать, ведь им не приходится управлять состоянием изнутри. А передачей состояний и обработчиков событий моделируются различные сценарии. - Однонаправленный поток данных: при поднятии состояния обеспечивается однонаправленный поток данных, при котором состояние передается от родителя вниз, а события обратно наверх, благодаря чему поток данных более предсказуем и прост в отладке.
- Больше контроля за жизненным циклом: Когда состояние управляется родителем, получается больше контроля над его жизненным циклом. Родителем определяется, когда и как изменяется состояние, благодаря чему повышаются производительность и эффективность управления такими ресурсами, как память.
Всеми этими преимуществами вместе совершенствуются общая структура, сопровождение и масштабируемость кодовой базы Jetpack Compose.
Резюмируем
Состояние ПИ поднимается к наименьшему общему предку всех composable, которыми это состояние считывается или изменяется. Максимальным приближением состояния к месту его использования поддерживаются четкое разделение обязанностей, эффективный поток данных. От владельца состояния пользователям предоставляется неизменяемое состояние вместе с событиями или обратными вызовами, благодаря которым при необходимости состояние изменяется. Подробнее — здесь.
3. Что такое «побочные эффекты»?
В Jetpack Compose это любые операции, которые сказываются на состоянии вне области видимости функции composable или длительно сохраняются после ее рекомпозиции. Эти composable спроектированы как чистые функции, которыми ПИ отображается на основе текущего состояния. Поэтому побочные эффекты применяются при выполнении таких действий вне жизненного цикла функции composable, как обновление общего состояния, активация однократных событий или взаимодействие с внешними ресурсами.
В Jetpack Compose эти сценарии безопасно и предсказуемо обрабатываются API-интерфейсами побочных эффектов: LaunchedEffect, SideEffect и DisposableEffect.
LaunchedEffect: для запуска корутин вcomposable
С LaunchedEffect корутина запускается в ответ на ключевые изменения состояния. Она выполняется внутри композиции Composition, а также отменяется и перезапускается при изменении указанного ключа, поэтому используется для одноразовых или реактивных задач вроде выборки данных или обработки анимации.
- Пример:
@Composable
fun MyScreen(userId: String) {
LaunchedEffect(userId) {
// Выполняется при изменении «userId» или вводе композиции
fetchDataForUser(userId)
}
}
2. SideEffect: для выполнения не перезапускаемых побочных эффектов
SideEffectвызывается при каждом успешном перекомпонованииcomposableи используется для выполнения легковесных, не перезапускаемых действий вроде обновления изменяемого общего объекта или логирования.- Пример:
@Composable
fun MyComposable(screenName: String) {
SideEffect {
// Выполняется после каждой рекомпозиции, идеален для аналитики или логирования
logScreenView(screenName)
}
}
3. DisposableEffect: для эффектов, которым требуется очистка
DisposableEffectиспользуется для действий, которым требуются настройка и очистка, таким как регистрация прослушивателя или ресурса, которые необходимо высвободить, когда композиция «покидает» экран или перекомпоновывается. Этим API определяется блокonDispose, вызываемый по завершении жизненного цикла функцииcomposable.- Пример:
@Composable
fun MyComposableWithListener(listener: SomeListener) {
DisposableEffect(listener) {
listener.register() // Вызывается при вводе композиции
onDispose {
listener.unregister() // Вызывается при выходе из композиции
}
}
}
Резюмируем
При корректном использовании этих API-интерфейсов побочных эффектов внешние ресурсы, события и изменения состояния эффективно управляются внутри жизненного цикла composable, поддерживается чистый и предсказуемый ПИ. Подробнее — в официальной документации Android по побочным эффектам в Compose.
- LaunchedEffect: на основе изменений состояния активируется корутина, идеально для асинхронных действий вроде загрузки данных.
- SideEffect: после каждой рекомпозиции выполняется не перезапускаемый код, приходится кстати для логирования или соответствующих действий.
- DisposableEffect: контролируются эффекты с требованиями настройки и очистки/удаления, такие как прослушиватели ресурсов.
Заключение
Вопросы собеседования для разработчиков Android сильно варьируются в зависимости от корпоративной культуры, сферы деятельности команды и даже самих интервьюеров. Подготовка затачивается под конкретную компанию или должность.
Все эти вопросы содержатся в приватном репозитории, где ежедневно делятся идеями об Android и Kotlin, Compose и архитектуре, вопросами собеседований и практическими рекомендациями относительно кода.
Читайте также:
- От кода до APK: полный разбор задач Android-сборки
- Хитрости объектно-ориентированного программирования. Часть 4: Шаблон Starter для Android
- Dynamic Feature Modules: навигация
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jaewoong Eum: Top Nine Android Developer Interview Questions You Should Know





