Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
10 подробных ответов
01Назови полный жизненный цикл Activity — какие колбэки и зачем нужен каждый?
junior
Короткий ответ: Три «поднимающих» колбэка — onCreate → onStart → onResume — выводят экран к пользователю; три «опускающих» — onPause → onStop → onDestroy — убирают. onRestart вызывается, когда остановленная Activity снова возвращается на экран.
Подробно:
onCreate() ← создан: setContentView, init, чтение savedInstanceState
│
onStart() ← стал видимым
│
onResume() ← на переднем плане, принимает ввод
│
[ RESUMED — работает ]
│
onPause() ← потерял фокус (частично перекрыт)
│
onStop() ← полностью не виден ──onRestart()→onStart() при возврате
│
onDestroy() ← уничтожен (finish или нехватка памяти)
- onCreate — единственный обязательный; здесь инфляция layout, инициализация, восстановление из
savedInstanceState. - onStart / onStop — граница видимости: регистрируем и снимаем то, что нужно только видимому экрану.
- onResume / onPause — граница фокуса и ввода; onPause обязан быть коротким — следующий экран не покажется, пока он не вернётся.
- onDestroy — финал; может не прийти при убийстве процесса, поэтому критичное сохраняем раньше.
⚠️ Частая ошибка: считать onDestroy гарантированным местом для сохранения. При process death его не будет — сохраняйте в onSaveInstanceState / onStop.
02launchMode: standard, singleTop, singleTask, singleInstance — чем отличаются и когда каждый уместен?
junior
Короткий ответ: standard создаёт новый экземпляр всегда; singleTop переиспользует Activity, только если она уже на вершине стека; singleTask держит единственный экземпляр как корень своей задачи; singleInstance — то же, но в отдельной задаче, где эта Activity одна.
Подробно:
| launchMode | Поведение | Когда |
|---|---|---|
| standard | новый экземпляр на каждый запуск | обычные экраны |
| singleTop | переиспользует, если наверху → onNewIntent | экран из нотификации/поиска |
| singleTask | один экземпляр, корень задачи, чистит всё над ним | точка входа, «главный» экран |
| singleInstance | один экземпляр в собственной задаче | звонилка, спец-экраны |
- onNewIntent — при переиспользовании (singleTop/singleTask) onCreate НЕ вызывается; свежий Intent приходит в
onNewIntent, и нужно вызватьsetIntent(it). - То же флагами Intent — задаётся динамически:
FLAG_ACTIVITY_NEW_TASK,FLAG_ACTIVITY_CLEAR_TOP,FLAG_ACTIVITY_SINGLE_TOP. - taskAffinity — определяет, в какую задачу попадёт Activity; вместе с singleTask/singleInstance управляет группировкой в Recents.
⚠️ Частая ошибка: ждать onCreate при singleTop-переиспользовании и читать данные из Intent старого запуска — свежий Intent приходит только в onNewIntent.
03BroadcastReceiver: статическая и динамическая регистрация — в чём разница и какие ограничения?
junior
Короткий ответ: Статическая регистрация — в манифесте, receiver может сработать, даже когда приложение не запущено. Динамическая — через registerReceiver() в коде, живёт, пока не вызовешь unregisterReceiver(). С Android 8 (API 26) манифест почти нельзя использовать для неявных (implicit) broadcast'ов.
Подробно:
| Статическая (манифест) | Динамическая (код) | |
|---|---|---|
| Регистрация | <receiver> в манифесте |
registerReceiver() |
| Живёт | даже без запущенного приложения | пока не unregisterReceiver() |
| Ограничение | с API 26 нельзя для implicit | привязана к lifecycle |
- Ограничение Oreo (API 26) — большинство неявных broadcast'ов из манифеста не доставляются; исключения — явные (адресные) и белый список вроде
BOOT_COMPLETED. - Динамическую — снимать —
registerReceiverв onStart/onResume,unregisterв onStop/onPause, иначе утечка Context. - Альтернативы — фоновая реакция → WorkManager с constraints; внутри приложения LocalBroadcastManager устарел, замена — Flow/StateFlow.
⚠️ Частая ошибка: полагаться на манифест-receiver для CONNECTIVITY_CHANGE — с API 26 он не придёт; регистрируйте динамически или используйте WorkManager.
04Какие колбэки Activity сработают при повороте экрана, нажатии Home, показе диалога поверх и кнопке Назад?
middle
Короткий ответ: Поворот — полное пересоздание. Home — onPause→onStop без destroy. Диалог поверх (AlertDialog/DialogFragment) — как правило вообще ничего: Activity остаётся RESUMED. Назад (выход) — onPause→onStop→onDestroy с isFinishing == true.
Подробно:
Поворот: onPause → onStop → onSaveInstanceState → onDestroy
→ onCreate → onStart → onRestoreInstanceState → onResume
Home: onPause → onStop (потом onRestart→onStart→onResume)
Back: onPause → onStop → onDestroy (isFinishing == true)
Диалог: — ничего — (AlertDialog / DialogFragment)
- Поворот (P+, API 28) — onSaveInstanceState вызывается ПОСЛЕ onStop; до Android P — до onStop и без гарантии относительно onPause. onRestoreInstanceState — после onStart.
- Home vs Back — оба проходят onStop, но Home не разрушает (
isFinishing == false), Back разрушает. - Диалог — это не Activity — AlertDialog рисуется в окне той же Activity, состояние не меняется. А вот прозрачная/диалоговая Activity сверху даёт onPause без onStop (хост частично виден).
⚠️ Частая ошибка: думать, что открытие диалога вызывает onPause. Нет — RESUMED сохраняется; onPause вызовет только другая Activity, вставшая поверх.
05Почему во фрагменте LiveData нужно наблюдать через viewLifecycleOwner, а не через this?
middle
Короткий ответ: View фрагмента живёт короче самого фрагмента: на бэкстеке onDestroyView уже прошёл, а инстанс фрагмента ещё жив. Если наблюдать LiveData с this (фрагментом), обсервер переживёт View; при возврате создастся новая View и второй обсервер — утечка старой View и двойные обновления.
Подробно:
// ❌ обсервер привязан к фрагменту — переживает View
viewModel.state.observe(this) { render(it) }
// ✓ обсервер снимается в onDestroyView вместе с View
viewModel.state.observe(viewLifecycleOwner) { render(it) }
Fragment: onAttach ─ onCreate ───────────────── onDestroy ─ onDetach
View: onCreateView ─ onViewCreated … onDestroyView
└──── жизнь viewLifecycleOwner ────┘
- Два разных owner'а — lifecycle самого фрагмента и lifecycle его View; на бэкстеке первый жив, второй уже мёртв.
- viewLifecycleOwner — привязывает подписку к окну onCreateView…onDestroyView и снимает её вовремя.
- ViewBinding — по той же причине
_bindingобнуляют в onDestroyView.
⚠️ Частая ошибка: observe(this) во фрагменте с бэкстеком — при каждом возврате обсерверы копятся поверх свежей View, а старая View течёт.
06onSaveInstanceState, ViewModel и постоянное хранилище — три уровня сохранения состояния: что где держать?
middle
Короткий ответ: ViewModel держит состояние в памяти и переживает поворот, но НЕ process death. onSaveInstanceState (и SavedStateHandle) кладёт немного данных в Bundle — переживает и поворот, и process death. Постоянное хранилище (DB/DataStore) — для того, что должно жить между запусками.
Подробно:
| Уровень | Переживает поворот | Переживает process death | Объём |
|---|---|---|---|
| ViewModel | да | нет | любой (в памяти) |
| SavedStateHandle / onSaveInstanceState | да | да | маленький (Bundle) |
| DB / DataStore | да | да | большой, постоянно |
class SearchViewModel(state: SavedStateHandle) : ViewModel() {
// переживёт и поворот, и убийство процесса
val query = state.getStateFlow("query", "")
}
- ViewModel — большой рабочий стейт экрана; чистится в
onCleared()при реальном уходе с экрана. - SavedStateHandle — навигационные аргументы, id, текст ввода: то, без чего экран после process death восстановится пустым. Только Parcelable/Serializable, не гигабайты.
- DB / DataStore — источник правды, а не «кэш состояния UI».
⚠️ Частая ошибка: складывать всё в ViewModel и удивляться пустому экрану после process death — ViewModel живёт в памяти, а память система забрала.
07Intent против PendingIntent — в чём разница и зачем с Android 12 обязательны флаги мутабельности?
middle
Короткий ответ: Intent — намерение, которое исполняет ваш собственный процесс. PendingIntent — токен-обёртка, который вы отдаёте чужому процессу (система, AlarmManager, нотификации), чтобы он выполнил действие ОТ ВАШЕГО ИМЕНИ и с вашими правами. С Android 12 (API 31) обязателен явный FLAG_IMMUTABLE или FLAG_MUTABLE.
Подробно:
val intent = Intent(ctx, ReplyReceiver::class.java)
val pi = PendingIntent.getBroadcast(
ctx, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) // API 31+: без FLAG_IMMUTABLE / FLAG_MUTABLE — крэш
- Чужой процесс, ваши права — нотификация живёт в SystemUI, но по тапу действие исполнится так, будто его запустили вы.
- FLAG_IMMUTABLE по умолчанию — безопасно;
FLAG_MUTABLEберут только когда получателю правда надо дописать extras (inline-ответы, бабблы). - requestCode + FLAG_UPDATE_CURRENT / FLAG_CANCEL_CURRENT — так система различает и обновляет «одинаковые» PendingIntent.
⚠️ Частая ошибка: ставить FLAG_MUTABLE «на всякий случай». Мутабельный PendingIntent с незаполненным таргетом — дыра: чужое приложение может подменить внутренний Intent.
08Service, Foreground Service и WorkManager — когда что выбрать и как менялись лимиты фоновой работы по версиям?
middle
Короткий ответ: Обычный Service — для короткой работы, пока приложение живо; с API 26 его нельзя запускать из фона. Foreground Service — работа, видимая пользователю прямо сейчас (плеер, навигация, запись), с обязательным уведомлением. WorkManager — гарантированная отложенная/периодическая работа, переживающая перезапуск устройства.
Подробно:
| Версия | Что изменилось |
|---|---|
| API 26 (O) | фоновые сервисы и manifest implicit broadcast запрещены; появился startForegroundService |
| API 31 (S) | нельзя стартовать FGS из фона → ForegroundServiceStartNotAllowedException |
| API 34 (U) | обязателен foregroundServiceType, проверка соответствия в рантайме |
- Правило выбора — нужно сейчас и видимо → FGS; можно потом, но обязательно → WorkManager; фоновая реакция на условие → WorkManager с constraints.
- startForegroundService — после него ОБЯЗАН вызвать
startForeground(id, notification)в течение ~5 c, иначе ANR/крэш. - WorkManager — сам выбирает JobScheduler под капотом и переживает ребут; для срочного — expedited work (WM 2.7+).
⚠️ Частая ошибка: тянуть тяжёлую фоновую загрузку на обычном Service — с API 26 система его прибьёт; это работа для WorkManager.
09Как ViewModel переживает поворот экрана? Опиши механизм под капотом.
senior
Короткий ответ: При смене конфигурации Activity действительно пересоздаётся, но перед уничтожением фреймворк вызывает onRetainNonConfigurationInstance и сохраняет объект NonConfigurationInstances с ViewModelStore. Новый экземпляр Activity достаёт тот же store через getLastNonConfigurationInstance — и получает те же ViewModel.
Подробно:
Activity #1 ──поворот──► onDestroy
│ isChangingConfigurations() == true → store НЕ очищается
▼
NonConfigurationInstances { ViewModelStore } ← держит фреймворк
│
▼
Activity #2 ──onCreate──► getLastNonConfigurationInstance()
→ тот же ViewModelStore → те же ViewModel
- ViewModelStore — по сути
HashMap<key, ViewModel>; ComponentActivity сам является ViewModelStoreOwner. - isChangingConfigurations() — ключевая развилка: при повороте
true, и onDestroy НЕ вызываетstore.clear(); при реальном finishfalse→clear()→onCleared()на каждой ViewModel. - Почему не переживает process death — NonConfigurationInstances живёт в памяти процесса; убили процесс — исчез и он. Отсюда и нужен SavedStateHandle.
⚠️ Частая ошибка: думать, что ViewModel «сохраняется системой». Нет — её просто НЕ уничтожают вместе с Activity; переживает только пересоздание внутри живого процесса.
10Что такое process death, почему после него приложение открывается на пустом экране и как это правильно тестировать?
senior
Короткий ответ: Process death — когда система, пока приложение в фоне, убивает его процесс ради памяти. Стирается вся память: ViewModel, статические поля, DI-синглтоны. При возврате система пересоздаёт последнюю Activity, но её стейт null → пустой или битый экран. Спасает только Bundle: onSaveInstanceState / SavedStateHandle.
Подробно:
Приложение в фоне ──► система убивает процесс (нужна память)
│ ViewModel, static, синглтоны — стёрты
▼
Пользователь вернулся ──► Activity пересоздаётся с savedInstanceState
восстановил из Bundle → ок; надеялся на память → пустой экран
- Как тестировать честно — приложение в фон, затем
adb shell am kill <package>или красная кнопка terminate в Android Studio; после этого вернуться в приложение. - «Don't keep activities» — НЕ то же самое — она разрушает Activity, но процесс и Application-стейт живы, поэтому баги на статике/синглтонах не ловит; для настоящего process death нужен
am kill. - Как чинить — критичные аргументы и ввод в SavedStateHandle; не держать «источник правды» в static-полях.
⚠️ Частая ошибка: восстанавливать экран из синглтона или статического поля — после process death оно пустое. Проверяйте именно убийство процесса, а не поворот.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.