На недавнем Google IO 2018 в числе прочего было представлено решение, помогающее в реализации навигации в приложениях.
Сразу бросилось в глаза то, что граф навигации можно просмотреть в UI редакторе, чем-то напоминающем сториборды из iOS-разработки. Это действительно интересно и ново для Android-разработчиков.
А так как я еще являюсь создателем довольно популярной библиотеки Cicerone, которая позволяет работать с навигацией на андроиде, то я получил множество вопросов на тему того, пора ли мигрировать на решение Google, и чем оно лучше/хуже. Поэтому, как только появилось свободное время, я создал тестовый проект и стал разбираться.
Постановка задачи
Я не буду повторять вводные слова про новый архитектурный компонент Navigation Architecture Component, про настройку проекта и основные сущности в библиотеке (это уже сделал Саша Блинов), а пойду с практической стороны и попробую библиотеку в бою, изучу возможности и применимость к реальным кейсам.
Я накидал на листе бумаги граф экранов с переходами, которые хотел бы реализовать.
Хочу отметить, что навигация в данном случае рассматривается в пределах одного Activity, такой подход я практикую уже не на первом проекте, и он себя отлично показал (обсуждение этого подхода — не цель данной статьи).
Таким графом я постарался описать наиболее частые кейсы, которые встречаются в приложениях:
- A — стартовый экран. С него приложение начинает работу при запуске из меню.
- Есть несколько прямолинейных переходов: A->B, B->C, C->E, B->F
- Из A можно запустить отдельный флоу a-b
- Из F можно переходить на новые экраны F, выстраивая цепочку одинаковых F-F-F-...
- Из F тоже можно запустить флоу a-b
- Флоу a-b — это последовательность двух экранов, где с экрана b можно выйти из флоу на тот экран, с которого был произведен запуск флоу
- Из экрана C можно перейти на D, заменив экран, то есть, из цепочки A-B-C, перейти к A-B-D
- Из экрана E можно перейти на D, заменив два экрана, то есть из цепочки A-B-C-E, перейти к A-B-D
- С экрана E можно вернуться на экран B
- С экрана D можно вернуться на экран A
Реализация
После переноса такого графа в код тестового Android-приложения получилось красиво:
На первый взгляд, очень похоже на ожидаемый результат, но давайте по порядку.
1. Стартовый экран устанавливается свойством
app:startDestination="@id/AFragment"
работает как и предполагалось, приложение запускается именно с него.
2. Прямолинейные переходы выглядят и работают тоже правильно
3. и 5. Вложенная навигация легко описывается и запускается как простой экран. И тут первое интересное свойство: я ожидал, что вложенная навигация будет как-то связана с вложенными фрагментами, но оказалось, что это просто удобное представление в UI-графе, а на практике все работает на одном общем стеке фрагментов.
4. Запуск цепочки одинаковых экранов делается без проблем. И тут можно воспользоваться специальным свойством перехода
app:launchSingleTop="true"
При указании этого режима перед переходом на новый экран будет проверка текущего верхнего экрана в стеке. Если новый и текущий экраны совпадают по типу, текущий экран будет удален, то есть верхний экран будет постоянно пересоздаваться, не увеличивая стек.
Тут скрыт первый баг: если открыть несколько раз экран F с параметром app:launchSingleTop="true"
, а потом нажать кнопку «назад», то будет произведен возврат на экран B, но экран F останется «висеть» сверху.
Могу предположить, что это происходит из-за того, что фрагмент менеджер внутри сохранил транзакцию с B на первый инстанс F, а мы его уже сменили на новый, который не будет ему известен.
6. Отдельный флоу из двух экранов работает прекрасно, но проблема в том, что нельзя явно описать поведение, что из второго экрана этого флоу надо вернуться на запустивший экран!
Здесь мы подошли к важному свойству UI-графа: на графе нельзя описать переходы назад! Все стрелки — это создание нового экрана и переход на него!
То есть чтобы сделать переход «назад» по стеку, надо обязательно работать с кодом. Самое простое решение для нашего случая — это вызвать два раза подряд popBackStack()
, новая навигация здесь ничем не помогает. А для более сложных флоу надо будет выдумывать другие решения.
Navigation.findNavController(view).apply {
popBackStack()
popBackStack()
}
7. Заменить экран C на D можно, указав в свойствах перехода
app:popUpTo="@+id/BFragment"
как видите, здесь мы жестко завязали логику перехода между C и D на предыдущий экран B. Тоже не самое лучшее место в новой библиотеке.
8. Аналогично и с переходом с E на D — добавляем app:popUpTo="@+id/BFragment"
, и работает как мы хотели, но с привязкой к экрану B
9. Как я объяснил выше, возвратов на UI-графе нет. Поэтому возврат с экрана E на экран B можно сделать только из кода, вызвав popBackStack(R.id.BFragment)
до необходимого экрана. Но если вам не обязательно именно вернуться на экран B, а допустимо создать новый экран B, то делается это переходом на UI-графе с установкой двух параметров:
app:popUpTo="@+id/BFragment"
app:popUpToInclusive="true"
Таким образом будет скинут стек до экрана B, включая его самого, а потом сверху создан новый экран B.
В реальных проектах такая логика конечно не подойдет, но как возможность — интересно.
10. Возврат на экран A, как вы уже поняли, невозможно описать на графе. Надо делать его из кода. Но по примеру предыдущего кейса, можно скинуть все экраны и создать новый экран A в корне. Для этого создаем переход из D на A и добавляем специальное свойство
app:clearTask="true"
Теперь при вызове такого перехода, сначала будет очищен весь стек фрагментов.
Остался один неизученный параметр из UI редактора навигации:
app:launchDocument="true"
Я так и не понял его предназначение, а скорее всего он просто не работает так, как заявлено в документации:
Launch a navigation target as a document if you want it to appear as its own entry in the system Overview screen. If the same document is launched multiple times it will not create a new task, it will bring the existing document task to the front.
If the user presses the system Back key from a new document task they will land on their previous task. If the user reached the document task from the system Overview screen they will be taken to their home screen.
Выводы
Из моих экспериментов и небольшого изучения исходников можно сделать вывод: новая навигация — это просто UI обертка работы с фрагмент менеджером (для случая приложения в одном Activity). Всем стрелкам-переходам выдается уникальный ID и набор параметров, по которым совершается транзакция фрагментов на старом «добром» фрагмент менеджере. Библиотека еще в глубокой альфе, но уже сейчас заметны баги, которые прямо указывают на известные проблемы фрагмент менеджера.
Если нам надо как-то повлиять на стек фрагментов и сделать возврат дальше предыдущего экрана, то придется работать по старинке с фрагмент менеджером.
Если вызвать навигацию при свернутом приложении, приложение упадет с классическим
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
Чтобы совершить навигацию нужен доступ к view для поиска navController
Navigation.findNavController(view)
Именно эти проблемы и решает библиотека Cicerone, поэтому сравнивать их мало смысла. Зато можно объединить! Потому что у Google-навигации есть и полезные стороны:
- Визуальный граф. Для небольших проектов может быть очень полезной подсказкой.
-
Описание входных параметров экрана и проверка без запуска приложения прямо в студии.
Для этого надо подключить дополнительный Gradle плагин и описывать обязательные параметры в xml свойствах экрана.<fragment android:id="@+id/confirmationFragment" android:name="com.example.cashdog.cashdog.ConfirmationFragment" android:label="fragment_confirmation" tools:layout="@layout/fragment_confirmation"> <argument android:name="amount" android:defaultValue=”0” app:type="integer" />
- Привязка Deep Link к запуску любого экрана из графа.
Мне не удалось проверить эту фичу, но есть подозрение, что она так же сделана в лоб, поэтому просто будет другой корневой экран, а не полная цепочка экранов, как хотелось бы. Но это не точно. - Возможность создавать переиспользуемые части графа — “Nested Graph”. То есть выносить некоторую часть графа во вложенный граф, который можно запускать из нескольких мест.
Тестовый проект
Все эксперименты можно посмотреть и потрогать на Github
Автор: terrakok