Архитектура Android
Фоновая работа в Android — Service, WorkManager и ограничения Doze.
10 вопросов
JuniorТеорияОчень частоЧто такое viewModelScope и lifecycleScope, и когда каждый из них отменяется?
Что такое viewModelScope и lifecycleScope, и когда каждый из них отменяется?
viewModelScope привязан к ViewModel и отменяется в onCleared(). lifecycleScope принадлежит Activity или Fragment и отменяется на onDestroy. Оба нужны, чтобы корутина не пережила запустивший её компонент, — поэтому в GlobalScope не запускают.
Типичные ошибки
- ✗Запускать корутину экрана в
GlobalScope, из-за чего она переживает уничтоженный компонент - ✗Думать, что
viewModelScopeотменяется на изменении конфигурации, а не вonCleared() - ✗Считать, что корутины отменённого scope сами возобновятся при возврате экрана
Уточняющие вопросы
- →Какой dispatcher
viewModelScopeиспользует по умолчанию и можно ли его сменить? - →Что происходит с корутиной, уже приостановленной внутри отменяемого scope?
JuniorТеорияОчень частоПереживает ли ViewModel поворот экрана и переживает ли он смерть процесса?
Переживает ли ViewModel поворот экрана и переживает ли он смерть процесса?
ViewModel привязан к ViewModelStoreOwner, поэтому переживает смену конфигурации: при повороте Activity пересоздаётся, но экземпляр остаётся тем же. Смерть процесса он не переживает — ОС убивает процесс вместе с памятью. Для этого и нужен SavedStateHandle.
Типичные ошибки
- ✗Считать, что
ViewModelпереживает и смерть процесса, поэтому состояние можно не сохранять - ✗Думать, что
ViewModelпересоздаётся при каждом повороте, как и егоActivity - ✗Путать удержание
ViewModelс механизмом saved-instanceBundle
Уточняющие вопросы
- →Что именно фреймворк удерживает через поворот, чтобы вернуть тот же
ViewModel? - →Какое состояние место в
SavedStateHandle, а не в обычном полеViewModel?
JuniorТеорияЧастоКогда выбрать WorkManager вместо foreground Service для фоновой работы?
Когда выбрать WorkManager вместо foreground Service для фоновой работы?
Используйте WorkManager для отложенной гарантированной работы, которая должна пережить смерть процесса и перезапуск приложения (он сохраняет и перепланирует задачи). Foreground Service берите для видимой пользователю работы, которая должна идти прямо сейчас и непрерывно — активное воспроизведение медиа или загрузка, за которой пользователь наблюдает.
Типичные ошибки
- ✗Думать, что
Serviceпо умолчанию работает не в главном потоке - ✗Считать, что
WorkManagerне умеет откладывать и сохранять работу через перезагрузку - ✗Использовать foreground
Serviceдля отложенной работы, которой не нужно идти сейчас
Уточняющие вопросы
- →Как режим Doze ограничивает
Serviceв сравнении сWorkManager? - →Почему foreground
Serviceобязан показывать постоянное уведомление?
JuniorТеорияЧастоКакова задача repository в слоистом Android-приложении и что он обязан скрывать?
Какова задача repository в слоистом Android-приложении и что он обязан скрывать?
Repository — единственная точка входа к данным фичи: он владеет сетевым и локальным источниками, решает, какой ответит на запрос, и отдаёт наверх доменные модели. Поэтому ViewModel не видит Retrofit или Room и тестируется с фейком.
Типичные ошибки
- ✗Инжектить Retrofit или Room прямо в
ViewModelи всё равно называть это repository - ✗Отдавать наверх сетевые DTO или сущности Room вместо доменных моделей
- ✗Считать repository кэшем, а не владельцем источников данных
Уточняющие вопросы
- →Как repository выбирает между локальным кэшем и свежим запросом в сеть?
- →Что repository отдаёт наверх, чтобы
ViewModelоставался тестируемым с фейком?
MiddleТеорияЧастоКак структурная конкурентность ложится на жизненный цикл Android и что когда отменяется?
Как структурная конкурентность ложится на жизненный цикл Android и что когда отменяется?
У каждого scope есть родительский Job, поэтому отмена scope отменяет все дочерние корутины, включая вложенные. lifecycleScope отменяется при каждом уничтожении Activity/Fragment, так что поворот его убивает; viewModelScope это переживает и отменяется только в onCleared(). Работа, которая должна пережить поворот, живёт в viewModelScope.
Типичные ошибки
- ✗Думать, что вложенный
launchпродолжает работать после отмены родительского scope - ✗Считать, что поворот отменяет
viewModelScopeтак же, как отменяетlifecycleScope - ✗Класть в
lifecycleScopeработу, которая должна пережить поворот, и терять её на каждом
Уточняющие вопросы
- →Что происходит с корутиной, приостановленной в точке отмены, когда её scope отменяют?
- →Почему
FragmentнуженviewLifecycleOwner.lifecycleScope, а не собственныйlifecycleScope?
MiddleДизайнЧастоВы делаете экран каталога товаров на Compose. Ограничения:
- На экране несколько взаимосвязанных кусков состояния — поисковый запрос, активные фильтры, порядок сортировки, постраничный список, флаг загрузки и баннер ошибки — и одно действие пользователя часто меняет сразу несколько из них.
- Любое изменение должно давать согласованный кадр: список, чипсы фильтров и флаг загрузки не должны противоречить друг другу на экране.
- QA должен уметь воспроизвести баг из записанной последовательности действий пользователя.
- Команда маленькая и не хочет писать руками много boilerplate.
Сравните архитектурный паттерн MVVM (ViewModel отдаёт несколько наблюдаемых холдеров состояния и публичные методы, которые зовёт view) с архитектурным паттерном MVI (каждое действие пользователя становится intent, который reducer превращает в один неизменяемый объект состояния). Скажите, кто владеет состоянием экрана, как действие пользователя до него доходит и какой паттерн вы выберете здесь и почему.
Вы делаете экран каталога товаров на Compose. Ограничения:
- На экране несколько взаимосвязанных кусков состояния — поисковый запрос, активные фильтры, порядок сортировки, постраничный список, флаг загрузки и баннер ошибки — и одно действие пользователя часто меняет сразу несколько из них.
- Любое изменение должно давать согласованный кадр: список, чипсы фильтров и флаг загрузки не должны противоречить друг другу на экране.
- QA должен уметь воспроизвести баг из записанной последовательности действий пользователя.
- Команда маленькая и не хочет писать руками много boilerplate.
Сравните архитектурный паттерн MVVM (ViewModel отдаёт несколько наблюдаемых холдеров состояния и публичные методы, которые зовёт view) с архитектурным паттерном MVI (каждое действие пользователя становится intent, который reducer превращает в один неизменяемый объект состояния). Скажите, кто владеет состоянием экрана, как действие пользователя до него доходит и какой паттерн вы выберете здесь и почему.
В MVVM ViewModel владеет несколькими независимыми холдерами состояния, а view прямо зовёт его методы — меньше церемоний, но поля могут разъехаться посреди обновления. MVI пропускает каждое действие через один поток intent в reducer, который выдаёт единый неизменяемый объект состояния: кадр всегда согласован, а баг воспроизводим по журналу. Цена — boilerplate; при таких связанных полях выбирайте MVI.
Типичные ошибки
- ✗Считать единое неизменяемое состояние чистой церемонией без выигрыша в согласованности
- ✗Дробить экран на много независимых холдеров, а потом бороться с кадрами, где они расходятся
- ✗Думать, что одна лишь рекомпозиция Compose гарантирует согласованность полей экрана
Уточняющие вопросы
- →Как reducer в архитектурном паттерне
MVIделает баг воспроизводимым по журналу? - →Когда boilerplate архитектурного паттерна
MVIне оправдан на простом экране?
SeniorДебаггингЧастоПочему этот Fragment течёт всей иерархией View при каждой навигации?
Почему этот Fragment течёт всей иерархией View при каждой навигации?
Созданный вручную scope никто не отменяет, поэтому собирающая корутина переживает onDestroyView и продолжает держать binding — на каждый визит удерживается мёртвая иерархия View. Собирайте из viewLifecycleOwner.lifecycleScope внутри repeatOnLifecycle(STARTED), который отменяется при смерти View, либо отменяйте scope сами в onDestroyView.
Типичные ошибки
- ✗Создавать
CoroutineScopeруками воFragmentи никогда его не отменять - ✗Собирать привязанный к View flow в
lifecycleScope, а не вviewLifecycleOwner.lifecycleScope - ✗Винить диспетчер или поле binding вместо неотменённого scope
Уточняющие вопросы
- →Почему
viewLifecycleOwner— правильный владелец для привязанного к View сбора воFragment? - →Что делает
repeatOnLifecycle(STARTED), чего не делает голыйlaunchв том же scope?
MiddleТеорияИногдаЧто SavedStateHandle сохраняет через смерть процесса, чего не сохраняет поле ViewModel?
Что SavedStateHandle сохраняет через смерть процесса, чего не сохраняет поле ViewModel?
SavedStateHandle — это map ключ-значение поверх saved-instance Bundle, который система пишет перед убийством процесса, поэтому его записи восстанавливаются в пересозданный ViewModel. Обычное поле ViewModel живёт только в памяти и пропадает. Он ограничен размером Bundle: держите там мелкие идентификаторы — запрос, выбранный id — а данные перезагружайте из repository.
Типичные ошибки
- ✗Считать, что обычное поле
ViewModelпереживает смерть процесса и сохранять нечего - ✗Класть в
SavedStateHandleвесь загруженный список вместо мелкого идентификатора - ✗Считать
SavedStateHandleпостоянным кэшем, который переживает смахивание из недавних
Уточняющие вопросы
- →Как отдать запись из
SavedStateHandleв UI как наблюдаемое состояние? - →Почему большое значение в
SavedStateHandleрискует получитьTransactionTooLargeException?
SeniorДизайнИногдаСпроектируйте фоновую синхронизацию для Android-приложения заметок: она забирает изменения с сервера и пишет их в локальную базу. Ограничения:
- Синхронизация должна рано или поздно выполниться, даже если пользователь больше не откроет приложение, и пережить смерть процесса и перезагрузку устройства.
- Запускаться она может только в безлимитной сети и только на зарядке и обязана остановиться, как только эти условия перестанут выполняться.
- Идёт примерно раз в час; двойная постановка в очередь не должна порождать две параллельные синхронизации.
- Ошибка сервера 503 или обрыв соединения — временные, их надо повторять с растущей задержкой; 401 — постоянная, её нельзя повторять бесконечно.
- Тело синхронизации написано на корутинах и должно быть отменяемым на каждом шаге.
Опишите выбранный механизм, как он переживает перезагрузку, как применяются условия устройства, как обрабатывается повторная постановка в очередь и как временная ошибка отличается от постоянной.
Спроектируйте фоновую синхронизацию для Android-приложения заметок: она забирает изменения с сервера и пишет их в локальную базу. Ограничения: - Синхронизация должна рано или поздно выполниться, даже если пользователь больше не откроет приложение, и пережить смерть процесса и перезагрузку устройства. - Запускаться она может только в безлимитной сети и только на зарядке и обязана остановиться, как только эти условия перестанут выполняться. - Идёт примерно раз в час; двойная постановка в очередь не должна порождать две параллельные синхронизации. - Ошибка сервера 503 или обрыв соединения — временные, их надо повторять с растущей задержкой; 401 — постоянная, её нельзя повторять бесконечно. - Тело синхронизации написано на корутинах и должно быть отменяемым на каждом шаге. Опишите выбранный механизм, как он переживает перезагрузку, как применяются условия устройства, как обрабатывается повторная постановка в очередь и как временная ошибка отличается от постоянной.
Поставьте в очередь PeriodicWorkRequest для CoroutineWorker с Constraints на безлимитную сеть и зарядку; WorkManager сохраняет запрос в собственной базе, поэтому он переживает смерть процесса и перезагрузку. Дедуп — через enqueueUniquePeriodicWork(KEEP). Возвращайте Result.retry() на 503 для экспоненциального backoff и Result.failure() на 401. doWork — suspend, поэтому он отменяется, когда условие перестаёт выполняться.
Типичные ошибки
- ✗Считать, что
Constraintsпроверяются только при постановке в очередь, а не во время работы - ✗Возвращать
Result.retry()на постоянную ошибку вроде 401, из-за чего задача повторяется вечно - ✗Полагаться на
AlarmManagerили цикл вGlobalScopeдля работы, обязанной пережить перезагрузку
Уточняющие вопросы
- →Какой backoff планировщик задач
WorkManagerприменяет кResult.retry()и как его настроить? - →Когда для уникальной работы правильнее
REPLACE, а неKEEP?
MiddleДизайнРедкоСпроектируйте upload manager для Android-приложения, загружающий фото и видео на сервер. Требования:
- Не блокировать UI-поток; загрузки идут в фоне, пока пользователь продолжает пользоваться приложением.
- Файлы большие (сотни МБ), поэтому загрузка может длиться много минут.
- Загрузка должна продолжаться, если пользователь свернул приложение, и в идеале пережить смерть процесса и возобновиться.
- Пользователя нужно оповестить о результате (успех или ошибка).
Укажите, какой механизм фонового выполнения вы выберете (foreground Service, планировщик задач WorkManager или оба), как он ведёт себя при энергосберегающих ограничениях Doze, как фоновый воркер сообщает прогресс и итоговый результат обратно в UI, и что станет с незавершённой загрузкой, если процесс приложения убьют посреди передачи.
Спроектируйте upload manager для Android-приложения, загружающий фото и видео на сервер. Требования:
- Не блокировать UI-поток; загрузки идут в фоне, пока пользователь продолжает пользоваться приложением.
- Файлы большие (сотни МБ), поэтому загрузка может длиться много минут.
- Загрузка должна продолжаться, если пользователь свернул приложение, и в идеале пережить смерть процесса и возобновиться.
- Пользователя нужно оповестить о результате (успех или ошибка).
Укажите, какой механизм фонового выполнения вы выберете (foreground Service, планировщик задач WorkManager или оба), как он ведёт себя при энергосберегающих ограничениях Doze, как фоновый воркер сообщает прогресс и итоговый результат обратно в UI, и что станет с незавершённой загрузкой, если процесс приложения убьют посреди передачи.
Используйте WorkManager с long-running воркером, повышенным через setForeground (он показывает обязательное постоянное уведомление). Он сохраняет задачу, поэтому она переживает смерть процесса и перепланируется после убийства. Doze позволяет foreground-воркеру работать. Прогресс сообщайте через setProgress, наблюдая WorkInfo в UI.
Типичные ошибки
- ✗Запускать многоминутную загрузку на сыром
Threadбез сохранения через смерть процесса - ✗Считать, что started
Serviceосвобождён от Doze без перехода в foreground - ✗Думать, что
START_STICKYпереотправляет исходные данные, а не nullIntent
Уточняющие вопросы
- →Почему long-running воркеру
WorkManagerприходится вызыватьsetForeground? - →Как возобновить частично загруженный большой файл, а не начинать заново?