Android backstack and navigation

Multiple back stacks

A deep dive into what actually went into this feature

If a ‘back stack’ is a set of screens that you can navigate back through via the system back button, ‘multiple back stacks’ is just a bunch of those, right? Well, that’s exactly what we’ve done with the multiple back stack support added in Navigation 2.4.0-alpha01 and Fragment 1.4.0-alpha01!

The joys of the system back button

Whether you’re using Android’s new gesture navigation system or the traditional navigation bar, the ability for users to go ‘back’ is a key part to the user experience on Android and doing that right is an important part to making your app feel like a natural part of the ecosystem.

In the simplest cases, the system back button just finishes your activity. While in the past you might have been tempted to override the onBackPressed() method of your activity to customize this behavior, it is 2021 and that is totally unnecessary. Instead, there are APIs for custom back navigation in the OnBackPressedDispatcher . This is actually the same API that FragmentManager and NavController already plug into.

That means when you use either Fragments or Navigation, they use the OnBackPressedDispatcher to ensure that if you’re using their back stack APIs, the system back button works to reverse each of the screens that you’ve pushed onto the back stack.

Multiple back stacks doesn’t change these fundamentals. The system back button is still a one directional command — ‘go back’. This has a profound effect on how the multiple back stack APIs work.

Multiple back stacks in Fragments

At the surface level, the support for multiple back stacks is deceptively straightforward, but requires a bit of an explanation of what actually is the ‘fragment back stack’. The FragmentManager ’s back stack isn’t made up of fragments, but instead is made up of fragment transactions. Specifically, the ones that have used the addToBackStack(String name) API.

This means when you commit() a fragment transaction with addToBackStack() , the FragmentManager is going to execute the transaction by going through and executing each of the operations (the replace , etc.) that you specified on the transaction, thus moving each fragment through to its expected state. FragmentManager then holds onto that transaction as part of its back stack.

When you call popBackStack() (either directly or via FragmentManager ’s integration with the system back button), the topmost transaction on the fragment back stack is reversed — an added fragment is removed, a hidden fragment is shown, etc. This puts the FragmentManager back into the same state that it was before the fragment transaction was initially committed.

Note: I cannot stress this enough, but you absolutely should never interleave transactions with addToBackStack() and transactions without in the same FragmentManager : transactions on your back stack are blissfully unaware of non-back stack changing fragment transactions — swapping things out from underneath those transactions makes that reversal when you pop a much more dicey proposition.

This means that popBackStack() is a destructive operation: any added fragment will have its state destroyed when that transaction is popped. This means you lose your view state, any saved instance state, and any ViewModel instances you’ve attached to that fragment are cleared. This is the main difference between that API and the new saveBackStack() . saveBackStack() does the same reversal that popping the transaction does, but it ensures that the view state, saved instance state, and ViewModel instances are all saved from destruction. This is how the restoreBackStack() API can later recreate those transactions and their fragments from the saved state and effectively ‘redo’ everything that was saved. Magic!

This didn’t come without paying down a lot of technical debt though.

Paying down our technical debts in Fragments

While fragments have always saved the Fragment’s view state, the only time that a fragment’s onSaveInstanceState() would be called would be when the Activity’s onSaveInstanceState() was called. To ensure that the saved instance state is saved when calling saveBackStack() , we need to also inject a call to onSaveInstanceState() at the right point in the fragment lifecycle transitions. We can’t call it too soon (your fragment should never have its state saved while it is still STARTED ), but not too late (you want to save the state before the fragment is destroyed).

Читайте также:  Как увеличить память андроид за счет памяти флешки

This requirement kicked off a process to fix how FragmentManager moves to state to make sure there’s one place that manages moving a fragment to its expected state and handles re-entrant behavior and all the state transitions that go into fragments.

35 changes and 6 months into that restructuring of fragments, it turned out that postponed fragments were seriously broken, leading to a world where postponed transactions were left floating in limbo — not actually committed and not actually not committed. Over 65 changes and another 5 months later, and we had completely rewritten most of the internals of how FragmentManager manages state, postponed transitions, and animations. That effort is covered in more detail in my previous blog post:

Источник

Welcome to another article in the second MAD Skills series on Navigation! In this article we’ll take a look at a highly requested feature, multiple back stack support for Navigation. If you prefer this content in video form, here is something to check out:

Intro

Let’s say your app uses BottomNavigationView . With this change, when the user selects another tab, the back stack for the current tab will be saved and the back stack for the selected tab will be restored seamlessly.

Starting with version 2.4.0-alpha01, the NavigationUI helpers support multiple back stacks without any code change. This means that if your app uses the setupWithNavController() methods for BottomNavigationView or NavigationView , all you need to do is to update the dependencies and multiple back stack support will be enabled by default.

Multiple Back Stack Support

Let’s see this in action using the Advanced Navigation sample from this repo.

The app consists of 3 tabs and each tab has its own navigation flow. To support multiple back stacks in earlier versions of Navigation, we needed to add a set of helpers in NavigationExtensions file to this sample. With these extensions, the app keeps a separate NavHostFragment with its own back stack for each tab and swaps between them as the user switches from one tab to another.

Let’s see what happens if I remove these extension functions. To do this I delete the NavigationExtensions class and remove all uses, switching over to the standard setupWithNavController() method from NavigationUI for connecting our BottomNavigationView to the NavController .

I also combine the 3 separate navigation graphs into a single graph by using the include tag. Now our activity’s layout just includes a single NavHostFragment with our single graph.

When I run the app, this time bottom tabs do not keep their state and reset its back stack as I switch to other tabs. With NavigationExtentions removed, the app lost multiple back stack support.

Now I update the version of navigation and fragment dependencies.

Once gradle sync is complete, I run the app again and I can see that each tab keeps its state when I navigate to another tab. Notice this behavior is enabled by default.

Finally, to verify that everything works, let’s run the tests. This app already has several tests to verify the multiple back stack behavior. I run BottomNavigationTest and watch different tests running and testing the bottom navigation behavior.

Voila, all our tests pass!

Summary

That’s it! If your app uses BottomNavigationView or NavigationView and you’ve been waiting for multiple back stack support, all you need to do is to update your navigation and fragment dependencies. No code change is needed!

If you’re doing something more custom, there are also new APIs to enable saving and restoring the back stack which you can learn more in this article.

If you are curious to learn more about the underlying APIs and what needed to be changed to support multiple back stacks, you can check out this article.

Читайте также:  Do all android tablets have bluetooth

Thanks for following this Navigation series!

Источник

Вы сейчас в четвертой части большого материала про Navigation Component в многомодульном проекте. Если вы уже знаете:

То добро пожаловать в заключительную часть истории о моем опыте с этой прекрасной библиотекой — про решение для iOS-like multistack-навигации.

Если не знаете, то выйдите и зайдите нормально прочитайте сначала три статьи выше.

В дополнение к библиотеке Navigation Component Google выпустили несколько интерфейсных дополнений под названием NavigationUI, которые помогут вам подключить навигацию к BottomBar, Menu и прочим стандартным компонентам. Но часто поступают требования, чтобы на каждой вкладке был свой стек и текущие состояния сохранялись при переходе между ними. К сожалению, из коробки Navigation Component и NavigationUI так не умеют.

Поддержку такого подхода представили сами Google в своем architecture-components-samples репозитории на GitHub (https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample). Суть его проста:

Создаем NavHostFragment и граф под каждую вкладку.

При выборе вкладки присоединяем необходимый NavHostFragment и отсоединяем текущий с помощью транзакций FragmentManager-a.

Но в ходе работы с этим решением я переделал некоторые моменты, связанные со спецификой проекта:

Многие приложения имеют sign in / up flow, on boarding и прочие экраны, которые не должны входить в стеки, но даже в таком случае все достаточно просто оборачивается стандартными средствами. Навигацию между этими частями можно выстроить уже как обычно, например, как в предыдущей части.

В примере все стеки инициализируются сразу при старте приложения. Связано это с корректной работой NavigationBottomBar и обработкой Deep Link-ов. Но я часто сталкивался с проектами, где deep link-и не нужны и бар навигации требует кастомизации. Проект, на котором я обкатывал подход — не исключение. Глядя на оригинальный файл NavigationExtensions в 250 loc, я решил выбросить все ненужное и сделать lazy-инициализацию NavHost-ов, оставив только основные функции:

Функция поиска / инициализации требуемого NavHost-фрагмента:

Функция смены NavHost-ов:

Функция своей обработки нажатия на кнопку “Back”:

В итоге

Таким образом мы получаем iOS-like навигацию и даже лучше, так как имеем lazy-нагрузку на стеках и меньшее количество кода. Приятный бонус — мы имеем полностью очевидную и прозрачную схему навигации, которую просто масштабировать и модифицировать.

Источник

Tasks и Back Stack в Android

Итак. Каждое Android приложение, как минимум, состоит из фундаментальных объектов системы — Activity. Activity — это отдельный экран который имеет свою отдельную логику и UI. Количество Activity в приложении бывает разное, от одного до много. При переходах между различными Activity пользователь всегда может вернуться на предыдущую, закрытую Activity при нажатии кнопки back на устройстве. Подобная логика реализована с помощью стека (Activity Stack). Его организация «last in, first out» — т.е. последний вошел, первый вышел. При открытии новой Activity она становится вершиной, а предыдущая уходит в режим stop. Стек не может перемешиваться, он имеет возможность добавления на вершину новой Activity и удаление верхней текущей. Одна и та же Activity может находиться в стеке, сколько угодно раз.
Task — это набор Activity. Каждый таск содержит свой стек. В стандартной ситуации, каждое приложение имеет свой таск и свой стек. При сворачивании приложения, таск уходит в background, но не умирает. Он хранит весь свой стек и при очередном открытии приложения через менеджер или через launcher, существующий таск восстановится и продолжит свою работу.

Ниже покажу картинку, как работает стек.

Если продолжать нажимать кнопку back, то стек будет удалять Activity до того, пока не останется главная корневая. Если же на ней пользователь нажмет back, приложение закроется и таск умрет. Кстати, я говорил о том, что когда мы сворачиваем наше приложение и запускам например новое, то наш таск просто уходит в background и будет ждать момента, пока мы его вызовем. На самом деле есть одно «но». Если мы будем иметь много задач в background или же просто сильно нагружать свое устройство, не мала вероятность того, что таск умрет из за нехватки системных ресурсов. Это не война конечно, но то что мы потеряем все наши текущие данные и наш стек очистится — это точно. Кстати для избежания потери данных в таком случаи, вам стоит почитать про SavingActivityState.

Маленький итог

Управление тасками

Существует два пути для изменения стандартной организации тасков. Мы можем устанавливать специальные атрибуты в манифесте для каждой Activity. Также мы можем устанавливать специальные флаги для Intent, который запускает новую Activity с помощью startActivity(). Заметьте, что иногда атрибуты в манифесте и флаги в Intent могут противоречить друг другу. В этом случаи флаги Intent будут более приоритетны.

Читайте также:  Системный интерфейс андроид что это

Атрибут launchMode

Для каждой Activity в манифесте можно указать атрибут launchMode. Он имеет несколько значений:

  • standard — (по умолчанию) при запуске Activity создается новый экземпляр в стеке. Activity может размещаться в стеке несколько раз
  • singleTop — Activity может распологаться в стеке несколько раз. Новая запись в стеке создается только в том случаи, если данная Activity не расположена в вершине стека. Если она на данный момент является вершиной, то у нее сработает onNewIntent() метод, но она не будет пересоздана
  • singleTask — создает новый таск и устанавливает Activity корнeвой для него, но только в случаи, если экземпляра данной Activity нет ни в одном другом таске. Если Activity уже расположена в каком либо таске, то откроется именно тот экземпляр и вызовется метод onNewIntent(). Она в свое время становится главной, и все верхние экземпляры удаляются, если они есть. Только один экземпляр такой Activity может существовать
  • singleInstance — тоже что и singleTask, но для данной Activity всегда будет создаваться отдельный таск и она будет в ней корневой. Данный флаг указывает, что Activity будет одним и единственным членом своего таска

На самом деле не важно, в каком таске открыта новая Activity. При нажатии кнопки back мы все равно вернемся на предыдущий таск и предыдущие Activity. Единственный момент, который нужно учитывать — это параметр singleTask. Если при открытии такой Activity мы достанем ее из другого background таска, то мы полностью переключаемся на него и на его стек. на картинке ниже это продемонстрировано.

Флаги

Как и говорил, мы можем устанавливать специальный флаги для Intent, который запускает новую Activity. Флаги более приоритетны, чем launchMode. Существует несколько флагов:

  • FLAG_ACTIVITY_NEW_TASK — запускает Activity в новом таске. Если уже существует таск с экземпляром данной Activity, то этот таск становится активным, и срабатываем метод onNewIntent().
    Флаг аналогичен параметру singleTop описанному выше
  • FLAG_ACTIVITY_SINGLE_TOP — если Activity запускает сама себя, т.е. она находится в вершине стека, то вместо создания нового экземпляра в стеке вызывается метод onNewIntent().
    Флаг аналогичен параметру singleTop описанному выше
  • FLAG_ACTIVITY_CLEAR_TOP — если экземпляр данной Activity уже существует в стеке данного таска, то все Activity, находящиеся поверх нее разрушаются и этот экземпляр становится вершиной стека. Также вызовется onNewIntent()

Affinity

Стандартно все Activity нашего приложения работают в одном таске. По желанию мы можем изменять такое поведение и указывать, чтобы в одном приложении Activity работали в разных тасках, или Activity разных приложений работали в одном. Для этого мы можем в манифесте для каждой Activity указывать название таска параметром taskAffinity. Это строковое значение, которое не должно совпадать с названием package, т.к. стандартный таск приложения называется именно как наш пакет. В общем случаи данный параметр указывает, что Activity будет гарантированно открываться в своём отдельном таске. Данный параметр актуален, если мы указываем флаг FLAG_ACTIVITY_NEW_TASK или устанавливаем для Activity атрибут allowTaskReparenting=«true». Этот атрибут указывает, что Activity может перемещаться между тасками, который её запустил и таском, который указан в taskAffinity, если один из них становится активным.

Чистка стека

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

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

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

Источник

Оцените статью