Введение

«Что такое back stack?» — короткий вопрос, за которым прячется половина понимания Android. По тому, как кандидат на него отвечает, сразу видно: он читал документацию или собирал приложение из сниппетов.

Back stack и задачи — это модель, на которой держится навигация: как экраны кладутся друг на друга, почему кнопка «Назад» возвращает именно на прошлый экран, что происходит с приложением, когда вы ушли на Home и вернулись через час. Без этой модели нельзя понять ни launch mode, ни флаги Intent, ни поведение onBackPressed — то есть всю вторую половину вопросов про навигацию.

На собеседовании эта тема всплывает в двух форматах: прямым вопросом («расскажите про back stack») или в связке с launch mode («что будет, если тут singleTask?»). Оба варианта закрываются одним и тем же знанием — как устроены задачи и стек.

Разберём по порядку: определение задачи, механика стека, жизнь задачи в фоне и инструменты управления — флаги и аффинити.

Что такое задача

Официальное определение из документации Android: задача — это набор Activities, с которыми пользователь взаимодействует, решая какое-то дело в приложении. Эти Activity выстроены в стек в порядке открытия — этот стек и называется back stack.

Ключевой момент: задача — это не «окно» и не «приложение». Одно приложение может жить в нескольких задачах, и одна задача может содержать Activity из разных приложений (например, вы открыли из своего приложения браузер — и он уехал в свою задачу).

Задача рождается так: пользователь нажимает на иконку приложения на лаунчере или на Home-экране. Если задачи для приложения ещё нет — система создаёт новую, и главная Activity приложения открывается как корневая (root) в стеке. Корневой считается Activity с intent-фильтром, в котором есть ACTION_MAIN и CATEGORY_LAUNCHER:

AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Именно эта пара — MAIN + LAUNCHER — делает Activity точкой входа в задачу из лаунчера.

Как устроен back stack

Механика стека простая, и её стоит рассказать чётко и по шагам:

  • когда текущая Activity запускает другую, новая кладётся на вершину стека и получает фокус, а предыдущая останавливается — но её UI-состояние система сохраняет;
  • кнопка или жест «Назад» снимает Activity с вершины и уничтожает её; предыдущая возобновляется, состояние её интерфейса восстанавливается;
  • Activity в стеке никогда не переставляются — только добавляются сверху и снимаются сверху. Стек работает как LIFO;
  • когда из стека убраны все Activity — задача перестаёт существовать.

Главное

Пока пользователь идёт вперёд — экраны копятся. Как только он идёт назад — экраны уничтожаются. Возврата «вперёд по истории» нет: стека под Activity не остаётся, после уничтожения она пересоздаётся заново. Это отличает Android-навигацию от веб-браузера.

Из этой механики следует неочевидное следствие: одна Activity может существовать в нескольких экземплярах сразу, даже в одной задаче. Если экран можно открыть из двух разных мест, каждый startActivity() создаст новый экземпляр и положит его наверх — предыдущие экземпляры останутся в стеке ниже. Поэтому в реальных приложениях появляются «дубли» экранов, и это не баг системы, а её поведение по умолчанию.

Ещё один нюанс про кнопку «Назад» на корневой Activity. На Android 12 и выше системное поведение изменилось: вместо завершения Activity система уводит её с задачей в фон — как будто пользователь нажал Home. Вернувшись, пользователь видит приложение в тёплом состоянии, без холодного рестарта. На Android 11 и ниже корневая Activity завершалась. Это свежая деталь, и её упоминание на собеседовании даёт плюс: кандидат следит за изменениями платформы.

Задачи в фоне: что происходит при сворачивании

Когда пользователь нажимает Home или открывает другое приложение, текущая задача уходит в фон. Все её Activity останавливаются, но back stack остаётся целым — задача просто теряет фокус. При возврате (через лаунчер или Recents) задача поднимается на передний план, и возобновляется Activity с вершины стека — пользователь продолжает ровно с того места, где остановился.

Задач в фоне может быть несколько одновременно. Но здесь есть оговорка, о которой часто забывают: если фоновых задач слишком много, система начинает уничтожать фоновые Activity, чтобы освободить память — и их состояния теряются. Никакой гарантии, что фоновый экран доживёт до возврата, нет.

Если пользователь долго не возвращается в задачу, система очищает её стек до корневой Activity — считается, что пользователь бросил старое дело и вернулся за новым. Поведение можно менять атрибутами alwaysRetainTaskState, clearTaskOnLaunch и finishOnTaskLaunch, но в реальном коде они встречаются редко — и это правильно.

Что важно сказать на собеседовании: жизненный цикл Activity и жизнь задачи — это два разных уровня. Activity отвечает за видимость и ресурсы, задача — за группировку Activity и сохранение стека. Путаница между ними — классический промах.

FLAG_ACTIVITY_NEW_TASK и аффинити

По умолчанию новая Activity кладётся в задачу вызывающего кода. Но это поведение можно изменить — флагом FLAG_ACTIVITY_NEW_TASK или атрибутом launchMode в манифесте.

Флаг FLAG_ACTIVITY_NEW_TASK говорит системе: запусти Activity в новой задаче. Точнее — и это важная деталь — система ищет задачу, к которой Activity «тяготеет». Здесь в игру вступает аффинити: характеристика Activity, обозначающая, в какую задачу она «предпочитает» попасть. По умолчанию все Activity одного приложения имеют одинаковую аффинити (имя пакета) и предпочитают жить в одной задаче.

FLAG_ACTIVITY_NEW_TASK ≠ «всегда новая задача». Система сначала ищет существующую задачу с той же аффинити: если нашла — запускает Activity туда (задача поднимается на передний план целиком, со своим back stack), если нет — создаёт новую задачу.

MainActivity.kt
val intent = Intent(this, ResultActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)

Типичный потребитель этого флага — уведомления: notification manager всегда стартует Activity во внешней задаче, поэтому всегда передаёт FLAG_ACTIVITY_NEW_TASK. И здесь же кроется требование: если Activity запускается этим флагом, у пользователя должен быть независимый способ вернуться в задачу — например, иконка на лаунчере (для этого корневой Activity и нужен фильтр LAUNCHER).

Аффинити можно менять атрибутом taskAffinity у <activity> — например, чтобы две группы экранов внутри одного приложения жили в разных задачах. Но честный совет: в большинстве проектов это не нужно, и на собеседовании достаточно сказать, зачем механизм существует, а не выдумывать сценарии применения.

Вопросы на собеседовании

Соберу типичные вопросы и короткие ориентиры для ответов — так удобнее готовиться.

  • «Что такое задача?» — набор Activity, объединённых одним делом пользователя; Activity лежат в back stack в порядке открытия.
  • «Что происходит с Activity при переходе вперёд и назад?» — вперёд: новая Activity на вершину, предыдущая останавливается с сохранением состояния. Назад: вершина снимается и уничтожается, предыдущая возобновляется.
  • «Может ли быть несколько экземпляров одной Activity?» — да, по умолчанию каждый запуск создаёт новый экземпляр, даже внутри одной задачи.
  • «Что будет с задачей, если уйти на Home?» — задача уйдёт в фон, стек сохранится; при нехватке памяти система может уничтожить фоновые Activity.
  • «Как заставить Activity открыться в новой задаче?»FLAG_ACTIVITY_NEW_TASK или launchMode="singleTask"; система будет искать задачу с той же аффинити.
  • «Чем задача отличается от приложения?» — приложение — это набор компонентов, задач может быть несколько, и в одну задачу могут попасть Activity из разных приложений.

Формула ответа

Задача = набор Activity в стеке → стек работает LIFO, Activity не переставляются → вперёд экраны копятся, назад — уничтожаются → в фоне стек цел, но без гарантий → флагами и launch mode задачу можно создавать и искать по аффинити.

Меня зовут Рустем Бикбулатов, я senior Android-разработчик и ментор Яндекс Практикума. Я провожу технические собеседования и за плечами у меня более 100 учеников и разборов — тема задач и back stack входит в тройку самых «топких» для кандидатов на middle, поэтому её стоит закрыть заранее.

Готовитесь к собеседованию на middle?

Бесплатный чек-лист подготовки к собеседованию — конкретные пункты, что закрыть и в каком порядке. Пришлю в Telegram без обязательств.

Итоги

Задачи и back stack — это фундамент навигации, и ответ на вопрос про них не должен быть заученным. Зафиксируем главное:

  • Задача — это коллекция Activity, выстроенная в back stack в порядке открытия. Стек работает как LIFO: Activity не переставляются, только кладутся наверх и снимаются с вершины.
  • Вперёд — экраны копятся, назад — уничтожаются. При «Назад» Activity уничтожается, предыдущая возобновляется с восстановленным состоянием. В фоне задача сохраняет стек, но система может уничтожить фоновые Activity при нехватке памяти.
  • Управление задачами — через FLAG_ACTIVITY_NEW_TASK и аффинити. Новая задача создаётся не всегда: система ищет задачу с той же аффинити и при нахождении поднимает её целиком.

Перед собеседованием

Попробуйте нарисовать на бумаге стек из трёх Activity и проговорить вслух: что произойдёт при «Назад», при Home, при возврате и при FLAG_ACTIVITY_NEW_TASK. Если вы можете это рассказать без подсказок — вопрос закрыт.

Если тема навигации — одна из тех, где вы чувствуете зыбкость, не нужно идти на собеседование в таком состоянии. Напишите в Telegram, разберём вашу ситуацию и составим план закрытия пробелов.