Одной из проблем, с которыми сталкивается разработчик немного подразросшегося приложения — навигация между экранами. Когда сценарии становятся нелинейными, уже тяжело обойтись стандартными startActivity и changeFragment. Эту проблему каждый решал по-своему: делал какое-то свое решение для навигации, использовал чужое решение (к примеру, Cicerone) или же оставлял все как есть и городил кучу флагов и if else. Это очень огорчало инженеров Google, и вот уже на Google I/O 2018 появилось решение Navigation, которое идёт в комплекте с остальными Архитектурными компонентами!
В данной статье мы разберемся, что вообще требуется от фреймворка для навигации, познакомимся с тем, как устроено решение от Google и какими абстракциями оно оперирует.
Требования к навигации
Начнём с требований, которые мы предъявляем фреймворку для навигации. В цикле статей мы будем к ним возвращаться, когда поймём, что они выполняются или нет.
Приоритет 1 (Обязательно):
[1] Отвязка от жизненного цикла.
[2] Возможность осуществлять вложенную навигацию.
[3] Автоматически открывает предыдущий экран при команде «Назад».
[4] Есть возможность добавить анимацию для смены экранов.
[5] Передача аргументов.
[6] Возможность перестраивать навигацию в рантайме.
[7] Поддержка Shared element.
[8] Возможность открывать и закрывать цепочку экранов.
Приоритет 2 (желательно):
[9] Удобный механизм работы с deeplink.
[10] Возможность имплементации на Activity, Fragment, View.
[11] Тестирование навигации.
[12] Гибкость при изменениях.
[13] Возможность навигации из бизнес-логики.
[14] Возможность подменять экраны, которые находятся в навигационном стеке.
[15] Проверка аргументов во время компиляции.
[16] Имеет визуальное представление для простого проектирования.
Постулаты навигации
Итак, для начала разберёмся с новыми принципами, которые диктует нам Google:
Принцип 1. Фиксированная точка старта
Все ссылки в приложение ведут в одну точку.Это означает, что мы всегда стартуем с одного экрана (по сути это SplashScreen), а дальше уже решаем, куда идти в приложении.
Принцип 2. Навигация работает в виде стека LIFO
Мы можем только делать операции push и pop, а экраны, которые лежат в середине стека, неприкосновенны.
Принцип 3. Кнопки Back и Up работают одинаково
Наконец-то стрелка тулбара и хардварная «Назад» будут работать одинаково! Больше не смущаем пользователей!
Принцип 4. Диплинки в приложение порождают стек экранов, аналогичный тому, если бы мы дошли до экрана самостоятельно
Если мы открыли приложение по диплинку, нажимая кнопку «Назад», мы пройдем по всем экранам, аналогично тому, как если бы мы их открывали без диплинка.
Определения
Для быстрого погружения можно скачать сэмпл с Github, чтобы попутно смотреть всё на практике.
В конце статьи также написано как побороть Android Studio, если сразу же не завелось.
Итак, как же устроен новый навигационный фреймворк. Он состоит из следующих компонентов:
Destination — экран, который открывается при навигации. Сохраним именно это обозначение, чтобы удобно было обращаться к документации. Это может быть Fragment, Activity, View и вообще всё, что вы подразумеваете под экраном навигации. В дальнейшем я буду говорить о фрагментах, но не забывайте, что можно использовать не только их.
NavHost интерфейс — это view-контейнер, по которому переключаются экраны (Destination), когда пользователь перемещается по приложению. Реализация по умолчанию для него — это NavHostFragment. В него посредством xml устанавливается граф для навигации.
NavGraph — это xml-предоставление навигации. В нём описываются экраны Destination и связи между ними.
NavController — сущность, которая осуществляет механизм навигации. Именно к нему мы будем обращаться, когда будем переключать между собой экраны (Destination). NavController устанавливается в NavHost. Если мы имеем дело с готовым NavHostFragment, то вся логика создания и предоставления NavController уже сделана за нас.
Action — команда переключения на другой экран. Её отправляем NavController-у для смены Destination
FragmentNavigator — внутренний класс, который инкапсулирует в себе транзакции фрагментов.
Принцип работы
Построенный NavGraph устанавливается в NavHostFragment (как частный случай NavHost). Теперь он знает, какие есть Action и какие экраны им соответствуют.
NavHostFragment предоставляет NavController, который сообщает ему о предстоящей транзакции. К NavController мы обращаемся из кода, отдавая нужную команду Action. За конечное переключение экранов отвечает FragmentNavigator, с которым взаимодействует NavHostFragment.
Имплементация
Настало время разобраться, как с этим зверем работать.
Для начала подключаем все необходимые библиотеки и плагины. Актуальную информацию проверяйте в соответствующем разделе на сайте Google.
buildscript {
ext.kotlin_version = '1.2.41'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-alpha14'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
dependencies {
def nav_version = '1.0.0-alpha01'
implementation "android.arch.navigation:navigation-fragment:$nav_version"
implementation "android.arch.navigation:navigation-ui:$nav_version"
//другие зависимости
}
Создаём навигационный граф
Далее создаём карту навигации. Для неё зафиксирован отдельный тип ресурса — navigation, для которого предусмотрен специальный редактор.
Для этого создаём файл res/navigation/nav_graph.xml с содержимым. Можно это сделать не руками, а через стандартный конструктор
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
</navigation>
Переключаем редактор на вкладку Design, и в нём мы накидываем экраны (Destination) и связываем их при помощи стрелок Actions.
Механика очень интуитивна и похожа на взаимодействие с ConstraintLayout. Если возникли проблемы, то можно посмотреть на детальный гайд от Google. (Выполнено требование [3]).
Конструктор состоит из двух видов сущностей: Destination (экраны) и Action (стрелочки).
Destination имеет набор атрибутов:
- ID — идентификатор экрана, для связи с другими посредством Action.
- Class — класс данного экрана (к примеру, LoginFragment).
Дополнительно имеется три группы параметров: - Arguments — аргументы, которые его параметризуют.
- Actions — команды, определяющие, куда можно перейти с текущего экрана.
- Deeplinks — диплинки для перехода на экран из-за пределов приложения.
Также существует возможность сделать этот экран стартовым.
Action имеет набор атрибутов.
- ID — идентификатор Action: его мы будем использовать для переключения экранов.
- Destination — ID Destination, на который ведёт Action.
Дополнительно имеется 4 группы параметров: - Transaction — анимация для переключения. Можно указать различные виды для действий: Enter, Exit, Pop Enter, Pop Exit. Анимации берутся из xml ресурсов. Легко определить свои, для этого просто добавьте соответствующий ресурс в папку res/anim. (См пример с nav_custom_enter_anim.xml)
- ArgumentDefaultValue — значение аргументов для фрагмента по умолчанию.
- PopBehavior — как навигации вести себя при закрытии фрагмента. К примеру, мы можем захотеть закрыть все экраны из стека и вернуться к стартовому. (См пример с Action action_notificationFragment_to_dashboardFragment — в нем мы как раз переходим на главный экран). (Выполнено требование [16])
- LaunchOption — опции по работе со стеком, напоминают launch Mode при работе со стеком Activity.
В итоге получается картинка похожая на титульную из статьи.
Привязываем навигационный граф к контейнеру
Навигационный граф есть, теперь привязываем его к контейнеру. Для этого добавляем в xml MainActivity, который выступает холдером для всей навигации NavHostFragment, и устанавливаем ему атрибуты:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout android:id="@+id/container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true" />
</FrameLayout>
Предпоследняя строчка говорит о том, что навигация будет осуществляться по созданному нами навигационному графу.
Последняя строчка означает, что NavHostFragment установлен дефолтным, это гарантирует обработку системного 'back'. Для выполнения принципа 3 навигации также необходимо переопределить в Activity действие на стрелку назад:
override fun onSupportNavigateUp()
= findNavController(R.id.nav_host_fragment).navigateUp()
Выполнение команды навигации
Для того, чтобы переключиться на другой фрагмент, нам необходимо получить Navigator.
Это возможно сделать несколькими способами.
NavHostFragment.findNavController(Fragment)
Navigation.findNavController(View)
Navigation.findNavController(Activity, @IdRes int viewId)
По сути, это команды из разных контекстов для поиска View, имплементирующую NavHost, и получения у неё NavController-а.
Далее у полученного NavController вызывается один из методов для навигации.
fragment_home_button_dashboard.setOnClickListener {
activity?.let {
findNavController(it, R.id.nav_host_fragment)
.navigate(R.id.action_homeFragment_to_dashboardFragment)
}
}
Для осуществления навигации при помощи NavController
у нас имеются в распоряжении:
- Несколько перегруженных методов navigate(). В них параметрами являются
- ActionId — тот самый ID Action из xml
- Bundle — набор аргументов,
- NavOptions — аргументы для Action, по которым определяется анимация, стек и пр.),
- NavDirections — абстракция, оборачивающая Bundle и ActionId
- Пара перегруженных методов popBackStack() для перехода к экранам из текущего стека.
В нашем примере используются только Action для перехода вперед. Команда 'Back' обрабатывается автоматически и вызывает обратное действие у Action (тот самый атрибут PopBehavior)
Так, при переходе назад после Action (команды) action_notificationFragment_to_dashboardFragment с экрана DashboardFragment мы попадаем не на NotificationFragment, а на HomeFragment.
<action
android:id="@+id/action_notificationFragment_to_dashboardFragment"
app:destination="@id/dashboardFragment"
app:enterAnim="@anim/nav_custom_enter_anim"
app:popUpTo="@+id/homeFragment" />
Итоги
На первый взгляд, фреймворк от Google не вызвал резкого осуждения и выглядит как нечто, на что необходимо посмотреть внимательнее.
В этой части мы удостоверились, что выполнены требования [3] и [16]. После первого взгляда, таблица наших требований к навигации для решения от Google выглядит так:
Приоритет 1 (обязательно):
[1] Отвязка от жизненного цикла.
[2] Возможность осуществлять вложенную навигацию.
[3] Автоматически открывает предыдущий экран при команде «Назад» [✓].
[4] Есть возможность добавить анимацию для смены экранов.
[5] Передача аргументов.
[6] Возможность перестраивать навигацию в рантайме.
[7] Поддержка Shared element.
[8] Возможность открывать и закрывать цепочку экранов.
Приоритет 2 (желательно):
[9] Удобный механизм работы с deeplink.
[10] Возможность имплементацию на Activity, Fragment, View.
[11] Тестирование навигации.
[12] Гибкость при изменениях.
[13] Возможность навигация из бизнес логики.
[14] Возможность подменять экраны которые находятся в навигационном стеке.
[15] Проверка аргументов во время компиляции.
[16] Имеет визуальное представление для простого проектирования [✓].
Что дальше?
В следующих частях рассмотрим (может быть порядок будет меняться):
- Транзакции для открытия фрагментов с аргументами.
- Диплинки.
- Встроенные средства для работы с BottomNavigation и Toolbar.
- Разные типы навигации: вложенную и с несколькими Activity.
- Как всё устроено внутри и насколько хорошо работает с жизненным циклом.
- Как сделать навигацию на вью.
- Как собрать навигационный граф из кода.
- Как тестировать навигацию.
- На сколько всё это готово к бою и какие абстракции надо добавить.
Как запустить прямо сейчас
Описанный пример лежит у меня на Github. Для того, чтобы его запустить, необходима Android Studio не ниже 3.2 Canary.
У меня завелось только с версией SDK Build Tools 28.0.0-rc1 (на 28.0.0-rc2 все отправлялось в бесконечную загрузку)
Если вдруг и это не помогло, то сделайте clean — меня в паре случаев выручало.
Автор: Александр Блинов