Задачи и обратный стек

Задача – это набор действий, которые пользователи выполняют в приложении. Эти действия упорядочены в обратном стеке в порядке их открытия.

Например, в почтовом приложении может быть одно действие для показа списка новых сообщений. Когда пользователь выбирает сообщение, открывается новое действие для просмотра этого сообщения. Это новое действие добавляется в обратный стек. Затем, когда пользователь нажимает или делает жест "Назад", это новое действие завершается и удаляется из стека.

Жизненный цикл задачи и ее обратный стек

Главный экран устройства – это отправная точка для большинства задач. Когда пользователь нажимает на значок приложения или ярлык на панели запуска приложений или на главном экране, задача этого приложения выходит на передний план. Если для приложения нет задачи, создается новая, а основное действие приложения открывается как корневое действие в стеке.

Когда текущее действие запускает другое, новое действие помещается в верхнюю часть стека и получает фокус. Предыдущее действие остается в стеке, но останавливается. Когда действие остановлено, система сохраняет текущее состояние его пользовательского интерфейса. Когда пользователь выполняет действие "Назад", текущее действие извлекается из верхней части стека и уничтожается. Предыдущее действие возобновляется, и восстанавливается предыдущее состояние его интерфейса.

Действия в стеке никогда не переставляются, а только добавляются при запуске и удаляются при закрытии с помощью кнопки или жеста "Назад". Таким образом, обратный стек работает по принципу последним пришел – первым ушел, как показано на рисунке 1.

Рисунок 1. Иллюстрация того, как каждое новое действие в задаче добавляет элемент в обратный стек. Когда пользователь нажимает кнопку "Назад" или выполняет соответствующий жест, текущее действие уничтожается, а предыдущее возобновляется.

По мере того как пользователь продолжает нажимать или жестикулировать назад, каждое действие в стеке выскакивает, чтобы показать предыдущее, пока пользователь не вернется на главный экран или к тому действию, которое выполнялось, когда задача началась. Когда все действия удалены из стопки, задача перестает существовать.

Поведение при нажатии кнопки "Назад" для корневых действий в лаунчере

Корневые действия запуска – это действия, которые объявляют фильтр интентов с ACTION_MAIN и CATEGORY_LAUNCHER. Эти объекты activity уникальны, поскольку служат точками входа в приложение из панели запуска приложений и используются для начала задачи.

Когда пользователь нажимает кнопку "Назад" или выполняет жест возврата в корневом действии панели запуска, система обрабатывает событие по-разному в зависимости от версии Android на устройстве.

Поведение системы на устройствах с Android 11 и более ранними версиями
Система завершает действие.
Поведение системы на устройствах с Android 12 и более поздними версиями ОС

Система перемещает действие и его задачу в фоновый режим, а не завершает действие. Это соответствует поведению системы по умолчанию при выходе из приложения с помощью кнопки или жеста главного экрана.

В большинстве случаев это позволяет пользователям быстрее возобновить работу с приложением из теплого состояния, а не перезапускать его из холодного состояния.

Если вам нужно настроить обратную навигацию, рекомендуем использовать API AndroidX Activity, а не переопределять onBackPressed(). Если нет компонентов, перехватывающих нажатие системной кнопки "Назад", AndroidX Activity API автоматически выбирает подходящее поведение системы.

Однако если ваше приложение переопределяет onBackPressed() для обработки обратной навигации и завершения действия, обновите реализацию, чтобы вызывать super.onBackPressed() вместо завершения. Вызов super.onBackPressed() перемещает действие и его задачу в фоновый режим, когда это необходимо, и обеспечивает более согласованную навигацию для пользователей в разных приложениях.

Фоновые и активные задачи

Рисунок 2. Две задачи: задача B получает пользовательское взаимодействие на переднем плане, а задача A находится в фоновом режиме и ожидает возобновления.

Задача – это единое целое, которое может перейти в фоновый режим, когда пользователь начинает новую задачу или переходит на главный экран. В фоновом режиме все действия в задаче останавливаются, но обратный стек для задачи остается нетронутым – задача теряет фокус, пока выполняется другая задача, как показано на рисунке 2. Затем задача может вернуться на передний план, чтобы пользователи могли продолжить с того места, где они остановились.

Рассмотрим следующую последовательность задач для текущей задачи A, в стеке которой есть три действия, включая два в текущем действии:

  1. Пользователь нажимает кнопку главного экрана или выполняет жест, а затем запускает новое приложение с панели запуска приложений.

    Когда появляется главный экран, задача А переходит в фоновый режим. При запуске нового приложения система запускает для него задачу (задача Б) с собственным стеком действий.

  2. После взаимодействия с этим приложением пользователь снова возвращается на главный экран и выбирает приложение, которое изначально запустило задачу А.

    Теперь задача А выходит на передний план – все три действия в ее стеке не затронуты, а действие в верхней части стека возобновляется. В этот момент пользователь также может вернуться к задаче B, перейдя на главный экран и выбрав значок приложения, которое запустило эту задачу, или выбрав задачу приложения на экране недавних приложений.

Несколько экземпляров действия

Рисунок 3. Один и тот же объект Activity можно создать несколько раз.

Поскольку действия в обратном стеке никогда не переупорядочиваются, если ваше приложение позволяет пользователям запускать определенное действие из нескольких действий, создается новый экземпляр этого действия и помещается в стек, а не выводится наверх какой-либо предыдущий экземпляр действия. Таким образом, одно действие в приложении может быть создано несколько раз, даже из разных задач, как показано на рисунке 3.

Если пользователь переходит назад с помощью кнопки или жеста "Назад", экземпляры действия отображаются в том порядке, в котором они были открыты, каждый со своим состоянием интерфейса. Однако вы можете изменить это поведение, если не хотите, чтобы действие создавалось более одного раза. Подробнее о том, как управлять задачами…

Многооконные среды

Когда приложения работают одновременно в многооконном режиме, который поддерживается в Android 7.0 (уровень API 24) и более поздних версиях, система управляет задачами отдельно для каждого окна. В каждом окне может быть несколько задач. То же самое относится к приложениям Android, работающим на Chromebook: система управляет задачами или группами задач для каждого окна.

Обзор жизненного цикла

Ниже приведены основные сведения о режиме работы по умолчанию для действий и задач.

  • Когда Activity A запускает Activity B, Activity A останавливается, но система сохраняет его состояние, например позицию прокрутки и текст, введенный в формы. Если пользователь нажмет или использует жест "Назад" в действии B, действие A возобновится с восстановленным состоянием.

  • Когда пользователь выходит из задачи с помощью кнопки главного экрана или жеста, текущее действие останавливается, а задача переходит в фоновый режим. Система сохраняет состояние каждого действия в задаче. Если пользователь позже возобновит задачу, выбрав значок приложения, с помощью которого она была запущена, задача выйдет на передний план и возобновит действие, находящееся вверху стека.

  • Если пользователь нажимает или жестикулирует назад, текущее действие извлекается из стека и уничтожается. Возобновляется предыдущее действие в стеке. Когда действие уничтожается, система не сохраняет его состояние.

    На устройствах с Android 12 (уровень API 31) и более поздними версиями ОС корневые действия запуска выполняются иначе.

  • Экземпляры действий можно создавать несколько раз, даже из других задач.

Управляйте задачами

Android управляет задачами и обратным стеком, помещая все действия, запущенные последовательно, в одну и ту же задачу, в стек, работающий по принципу "последним пришел – первым ушел". Это отлично работает для большинства приложений, и вам обычно не нужно беспокоиться о том, как ваши действия связаны с задачами или как они существуют в обратном стеке.

Однако вы можете прервать стандартное поведение. Например, вы можете захотеть, чтобы действие в вашем приложении начинало новую задачу при запуске, а не помещалось в текущую задачу. Или, когда вы запускаете действие, вы можете захотеть выдвинуть на передний план существующий экземпляр этого действия, а не создавать новый экземпляр поверх обратного стека. Или вы можете захотеть, чтобы ваш обратный стек был очищен от всех действий, кроме корневого действия, когда пользователь покидает задачу.

Для этого и других действий можно использовать атрибуты в элементе манифеста <activity> и флаги в намерении, которое вы передаете в startActivity().

Ниже перечислены основные атрибуты <activity>, которые можно использовать для управления задачами.

Ниже перечислены основные флаги, которые можно использовать.

В следующих разделах рассказывается, как использовать эти атрибуты манифеста и флаги намерений, чтобы определить, как действия связываются с задачами и как они ведут себя в обратном стеке.

В следующих разделах также рассказывается, как система представляет задачи и действия на экране "Недавние" и управляет ими. Обычно вы позволяете системе определять, как ваша задача и действия представлены на экране недавних, и вам не нужно изменять это поведение. Подробнее об экране "Недавние"…

Определите режимы запуска

Режимы запуска позволяют определить, как новый экземпляр действия связан с текущей задачей. Режимы запуска можно задавать двумя способами, описанными в следующих разделах:

Таким образом, если действие A запускает действие B, действие B может определить в своем манифесте, как оно связывается с текущей задачей, а действие A может использовать флаг намерения, чтобы запросить, как действие B может связаться с текущей задачей.

Если оба действия определяют, как действие B связывается с задачей, то запрос действия A, определенный в намерении, имеет приоритет над запросом действия B, определенным в его манифесте.

Как задать режимы запуска в файле манифеста

При объявлении действия в файле манифеста вы можете указать, как действие связывается с задачей, используя атрибут launchMode элемента <activity>.

Атрибуту launchMode можно назначить пять режимов запуска:

  1. "standard"
    Режим по умолчанию. Система создает новый экземпляр действия в задаче, из которой оно было запущено, и направляет ему намерение. Для действия можно создать несколько экземпляров, каждый из которых может относиться к разным задачам, а у одной задачи может быть несколько экземпляров.
  2. "singleTop"
    Если экземпляр действия уже существует в верхней части текущей задачи, система направляет намерение этому экземпляру через вызов его метода onNewIntent(), а не создает новый экземпляр действия. Экземпляры действия создаются несколько раз. Каждый из них может принадлежать к разным задачам, а одна задача может иметь несколько экземпляров (но только если действие в верхней части обратного стека не является существующим экземпляром действия).

    Например, предположим, что обратный стек задачи состоит из корневого действия A с действиями B, C и D сверху (таким образом, стек представляет собой A-B-C-D, где D находится сверху). Намерение поступает для действия типа D. Если у D задан режим запуска по умолчанию ("standard"), запускается новый экземпляр класса, а стек становится таким: A-B-C-D-D. Однако если для D задан режим запуска "singleTop", существующий экземпляр D получает намерение через onNewIntent(), поскольку он находится вверху стека, и стек остается в виде A-B-C-D. Если же намерение поступает для действия типа B, то в стек добавляется новый экземпляр B, даже если его режим запуска – "singleTop".

  3. "singleTask"
    Система создает действие в корне новой задачи или находит действие в существующей задаче с тем же сходством. Если экземпляр действия уже существует, система направляет намерение в существующий экземпляр через вызов его метода onNewIntent(), а не создает новый экземпляр. При этом все остальные действия, которые находятся поверх него, удаляются.
  4. "singleInstance"
    Поведение аналогично "singleTask", за исключением того, что система не запускает другие действия в задаче, содержащей экземпляр. Активность всегда является единственным элементом своей задачи. Любые действия, запущенные этим, открываются в отдельной задаче.
  5. "singleInstancePerTask"
    Действие может выполняться только как корневое действие задачи, то есть первое действие, создавшее задачу. Таким образом, в задаче может быть только один экземпляр этого действия. В отличие от режима запуска singleTask, это действие может быть запущено в нескольких экземплярах в разных задачах, если установлен флаг FLAG_ACTIVITY_MULTIPLE_TASK или FLAG_ACTIVITY_NEW_DOCUMENT.

Например, в браузере Android в элементе <activity> указано значение singleTask, поэтому его действия не помещаются в задачу вашего приложения. Вместо этого либо запускается новая задача для браузера, либо на передний план выходит существующая фоновая задача браузера.

Независимо от того, запускается ли объект activity в новой задаче или в той же задаче, что и объект activity, который его запустил, кнопка "Назад" и жест всегда возвращают пользователя к предыдущему объекту activity. Однако если вы запускаете действие, для которого задан режим запуска singleTask, и экземпляр этого действия существует в фоновой задаче, то вся эта задача переносится на передний план. В этот момент обратный стек включает все действия из задачи, выдвинутой на передний план в верхней части стека. Этот сценарий показан на рисунке 4.

Рисунок 4. Схема, показывающая, как действие с режимом запуска "singleTask" добавляется в обратный стек. Если действие уже является частью фоновой задачи с собственным обратным стеком, то весь этот обратный стек также перемещается вперед, поверх текущей задачи.

Дополнительную информацию об использовании режимов запуска в файле манифеста можно найти в документации по элементу <activity>.

Как задать режимы запуска с помощью флагов Intent

При запуске действия вы можете изменить его связь с задачей по умолчанию, включив флаги в намерение, которое вы доставляете в startActivity(). Для изменения поведения по умолчанию можно использовать следующие флаги:

FLAG_ACTIVITY_NEW_TASK

Система запускает действие в новой задаче. Если для запускаемой операции уже выполняется задача, она переводится на передний план с восстановлением последнего состояния, а операция получает новый объект Intent в методе onNewIntent().

Это приводит к тому же результату, что и значение launchMode атрибута "singleTask", описанное в предыдущем разделе.

FLAG_ACTIVITY_SINGLE_TOP

Если запускаемое действие является текущим и находится вверху обратного стека, то вместо создания нового экземпляра действия существующий экземпляр получает вызов onNewIntent().

Это приводит к тому же результату, что и значение "singleTop"launchMode, описанное в предыдущем разделе.

FLAG_ACTIVITY_CLEAR_TOP

Если запускаемая операция уже выполняется в текущей задаче, то вместо запуска нового экземпляра этой операции система уничтожает все остальные операции, находящиеся поверх нее. Намерение доставляется в возобновленный экземпляр действия, который теперь находится вверху, через onNewIntent().

Для атрибута launchMode нет значения, которое бы приводило к такому поведению.

FLAG_ACTIVITY_CLEAR_TOP чаще всего используется в сочетании с FLAG_ACTIVITY_NEW_TASK. При совместном использовании эти флаги находят существующее действие в другой задаче и помещают его в позицию, в которой оно может ответить на намерение.

Как обрабатывать сходства

Родство указывает, к какой задаче "предпочитает" относиться действие. По умолчанию все действия из одного приложения имеют сходство друг с другом: они "предпочитают" находиться в одной задаче.

Однако вы можете изменить настройки по умолчанию для действия. Действия, определенные в разных приложениях, могут иметь одинаковое сходство, а действиям, определенным в одном приложении, могут быть назначены разные сходства задач.

Вы можете изменить сходство действия с помощью атрибута taskAffinity элемента <activity>.

Атрибут taskAffinity принимает строковое значение, которое должно отличаться от названия пакета по умолчанию, объявленного в элементе <manifest>, поскольку система использует это название для определения родственной задачи по умолчанию для приложения.

Связь учитывается в двух случаях:

  1. Если намерение, запускающее действие, содержит флаг FLAG_ACTIVITY_NEW_TASK.

    По умолчанию новое действие запускается в задаче действия, которое вызвало startActivity(). Оно помещается в тот же обратный стек, что и вызывающий объект.

    Однако если намерение, переданное в startActivity(), содержит флаг FLAG_ACTIVITY_NEW_TASK, система ищет другую задачу для размещения нового действия. Часто это новая задача. Однако это не обязательно. Если существует задача с тем же значением affinity, что и у нового действия, действие запускается в этой задаче. Если нет, то начинается новая задача.

    Если этот флаг заставляет действие начать новую задачу, а пользователь использует кнопку главного экрана или жест, чтобы выйти из нее, у пользователя должен быть способ вернуться к задаче. Некоторые объекты, например менеджер уведомлений, всегда запускают действия во внешней задаче, а не в своей собственной, поэтому они всегда добавляют FLAG_ACTIVITY_NEW_TASK в намерения, которые передают в startActivity().

    Если внешняя сущность может вызвать вашу активность с этим флагом, предоставьте пользователям независимый способ вернуться, например значок приложения из фильтра CATEGORY_LAUNCHER в корневой активности задачи. Подробнее о том, как начать задачу…

  2. Если атрибут allowTaskReparenting действия имеет значение "true".

    В этом случае действие может переместиться из задачи, в которой оно начинается, в задачу, с которой оно связано, когда эта задача выходит на передний план.

    Например, в приложении для путешествий есть действие, которое сообщает о погоде в выбранных городах. Оно имеет такое же сходство, как и другие действия в том же приложении, то есть сходство приложения по умолчанию, и может быть переподчинено с помощью этого атрибута.

    Когда одно из ваших действий запускает действие прогноза погоды, оно изначально принадлежит той же задаче, что и ваше действие. Однако когда задача приложения для путешествий выходит на передний план, действие прогноза погоды переназначается этой задаче и отображается в ней.

Очистка обратного стека

Если пользователь долго не возвращается к задаче, система удаляет из нее все действия, кроме корневого. Когда пользователь возвращается к задаче, восстанавливается только корневое действие. Система ведет себя таким образом, исходя из предположения, что по прошествии длительного времени пользователи отказались от того, что они делали раньше, и возвращаются к задаче, чтобы начать что-то новое.

Чтобы изменить это поведение, можно использовать следующие атрибуты действий:

alwaysRetainTaskState
Если для этого атрибута задано значение "true" в корневом объекте activity задачи, описанный выше режим работы по умолчанию не происходит. Задача сохраняет все действия в своем стеке даже по прошествии длительного времени.
clearTaskOnLaunch

Если для этого атрибута в корневом действии задачи задано значение "true", то при каждом выходе пользователя из задачи и возвращении к ней задача очищается до корневого действия. Другими словами, это противоположность alwaysRetainTaskState. Пользователь всегда возвращается к задаче в ее исходном состоянии, даже если он покинул задачу лишь на мгновение.

finishOnTaskLaunch

Этот атрибут похож на clearTaskOnLaunch, но работает с одним действием, а не со всей задачей. Он также может привести к завершению любого действия, кроме корневого. Если задано значение "true", действие остается частью задачи только в течение текущего сеанса. Если пользователь уходит, а затем возвращается к задаче, она больше не присутствует.

Как начать задачу

Чтобы сделать действие точкой входа для задачи, добавьте в него фильтр интентов с действием "android.intent.action.MAIN" и категорией "android.intent.category.LAUNCHER":

<activity ... >
    <intent-filter ... >
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    ...
</activity>

Этот фильтр намерений показывает значок и ярлык для действия в панели запуска приложений, позволяя пользователям запускать действие и возвращаться к созданной им задаче.

Эта вторая способность важна. Пользователи должны иметь возможность покинуть задачу, а затем вернуться к ней позже, используя эту панель запуска. Поэтому используйте только два режима запуска, которые помечают действия как всегда инициирующие задачу, "singleTask" и "singleInstance", когда у действия есть фильтр ACTION_MAIN и CATEGORY_LAUNCHER.

Представьте, что произойдет, если фильтр отсутствует: намерение запускает действие "singleTask", инициируя новую задачу, и пользователь тратит некоторое время на работу над этой задачей. Затем пользователь нажимает кнопку главного экрана или выполняет жест. Задача будет выполняться в фоновом режиме. Теперь у пользователя нет возможности вернуться к задаче, потому что она не представлена в панели запуска приложений.

Если вы не хотите, чтобы пользователь мог вернуться к действию, задайте для атрибута finishOnTaskLaunch элемента <activity> значение "true". Подробнее о том, как очистить обратный стек…

Подробнее о том, как задачи и действия представлены на экране "Недавние" и как ими управлять…

Дополнительные ресурсы