В этом уроке рассмотрим простой пример использования Navigation Architecture Component.
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
На Google IO был представлен Navigation Architecture Component. На сегодняшний день (28.05.2018) он в альфа версии, поэтому использовать его в рабочих проектах пока рановато, но ознакомиться уже можно. Ближайшие уроков 5 будут о нем.
Я подробно просмотрел исходники этого компонента, чтобы точно понимать, что он делает. Под капотом там все те же startActivity и FragmentManager. Navigation Architecture Component - это обертка над этими стандартными механизмами, которая призвана упорядочить и упростить навигацию в приложении.
Для полноценной работы с Navigation нам понадобится Android Studio 3.2. Пока что есть только Canary сборка, она вполне подойдет.
Navigation функционал может быть выключен по умолчанию. Проверьте настройки студии Settings > Experimental > Enable Navigation Editor. Перезапустите студию после включения этого чекбокса.
dependencies в build.gradle файле модуля:
implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha02'
Рассмотрим простой пример с MainActivity и тремя фрагментами: Fragment1, Fragment2, Fragment3. MainActivity будет поочередно отображать в себе фрагменты.
Для этого нам понадобятся следующие Navigation инструменты:
NavController - этот основной механизм Navigation Component. Именно его мы будем просить показывать на экране фрагменты. Но чтобы он мог это делать, он должен иметь список фрагментов и контейнер, в котором он будет эти фрагменты отображать.
NavGraph - это список фрагментов, который мы будем создавать и наполнять. NavController сможет показывать фрагменты только из этого списка. Далее будем называть его графом.
NavHostFragment - это контейнер. Внутри него NavController будет отображать фрагменты.
Еще раз, кратко, для понимания: контроллер в контейнере отображает фрагменты из графа.
Начнем с создания графа (NavGraph). Это обычный resource файл с типом Navigation.
После создания он пуст:
Давайте добавлять фрагменты. В терминологии Navigation они называются destination.
Жмем кнопку добавления, студия показывает нам фрагменты и Activity, которые есть в проекте. Добавляем три фрагмента.
Результат
Слева видим список всех destination в этом графе. Пометкой Start отмечен стартовый destination, который сразу будет отображен при запуске приложения. В нашем случае это Fragment1.
Посередине отображены те же destination, но уже не списком, а в их реальном виде, с использованием их layout. Значком домика помечен стартовый destination. Для всех трех фрагментов я создал одинаковые layout: название фрагмента и пара кнопок. Позже будем использовать эти кнопки для навигации.
Справа находится панель атрибутов для текущего выделенного destination. О них мы подробно поговорим позже. Пока что нас интересует атрибут ID. Этот ID нам надо будет сообщать контроллеру (NavController), чтобы он отобразил соответствующий фрагмент. Этот ID кстати отображается в панели посередине. Над каждым фрагментом можно увидеть его ID.
Ок, граф создан. Теперь в MainActivity надо добавить контейнер (NavHostFragment), в котором NavController будет отображать фрагменты.
В activity_main добавляем NavHostFragment:
<FrameLayout 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:defaultNavHost="true" app:navGraph="@navigation/main_graph" /> </FrameLayout>
Контейнер готов. Остается где-то взять контроллер (NavController). Тут нам поможет контейнер. Он при создании сам создаст контроллер и чуть позже поделится им с нами.
Обратите внимание, что в атрибуте navGraph мы указали созданный ранее граф main_graph. Контейнер передаст этот граф контроллеру.
Переходим к коду.
Чтобы попросить контроллер у контейнера, используем метод Navigation.findNavController с указанием ID контейнера. Этот метод по ID найдет контейнер NavHostFragment и возьмет у него контроллер.
Код в MainActivity.java
NavController navController; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); navController = Navigation.findNavController(this, R.id.nav_host_fragment); }
Теперь мы можем использовать этот контроллер для навигации по фрагментам. Для этого есть два метода:
navigate(int resId) - чтобы открыть какой-либо фрагмент из графа, надо у контроллера вызвать метод navigate и передать ему ID destination. Контроллер просмотрит граф, определит какому фрагменту в графе соответствует ID и отобразит этот фрагмент.
popBackStack - возврат на шаг назад, на предыдущий фрагмент.
Как вы уже видели, в каждом из трех фрагментов есть кнопки Back и Next. По нажатию на кнопку Next мы будем открывать следующий фрагмент. А по нажатию на кнопку Back будем возвращаться на предыдущий. Я использовал колбэки и обработку нажатий на эти кнопки вытащил в Activity. Соответственно в MainActivity у меня 6 методов (3 фрагмента, 2 кнопки в каждом)
В этих методах мы и будем работать с контроллером.
@Override public void onFragment1NextClick() { navController.navigate(R.id.fragment2); } @Override public void onFragment1BackClick() {} @Override public void onFragment2NextClick() { navController.navigate(R.id.fragment3); } @Override public void onFragment2BackClick() { navController.popBackStack(); } @Override public void onFragment3NextClick() {} @Override public void onFragment3BackClick() { navController.popBackStack(); }
По названию метода понятно для какой кнопки какого фрагмента он является обработчиком.
Например, по нажатию на Next в Fragment1 мы просим контроллер открыть destination с ID = fragment2. Контроллер найдет этот destination в графе, увидит, что это фрагмент Fragment2 и в контейнере отобразит этот фрагмент.
Аналогично по нажатию на Next в Fragment2 мы просим открыть destination с ID = fragment3, который в графе соответствует фрагменту Fragment3.
По нажатию на кнопки Back, мы возвращаемся на шаг назад.
Запускаем приложение. При старте сразу отобразится Fragment1, потому что он является стартовым в графе.
Выполняем навигацию.
Системная кнопка Back тоже работает и выполняет шаг назад. Это происходит благодаря атрибуту
app:defaultNavHost="true"
который мы указали в контейнере (NavHostFragment). Контейнер перехватывает нажатия и показывает предыдущий фрагмент.
Если установить его значение в false, то контейнер больше не будет обрабатывать системную кнопку Back, и Activity будет закрываться.
Action
При вызове destination нам может понадобиться задать некоторые параметры, например, анимацию, аргументы и т.п. Для этого используется action.
Давайте создадим action, который будет выполнять переход от fragment1 к fragment2
Для destination fragment1 мы создали action, который ведет в destination fragment2.
У action есть различные параметры, которые мы можем настраивать в редакторе графа. Они будут использованы при переходе от destination fragment1 к destination fragment2.
Мы разберем их подробно в следующих уроках. Пока нас снова интересует только значение атрибута ID. Мы можем использовать его при вызове метода navigate, чтобы вызвать action. Давайте сделаем это по нажатию на кнопку Next в Fragment1.
@Override public void onFragment1NextClick() { navController.navigate(R.id.action_fragment1_to_fragment2); }
Контроллер сделает следующее:
1) возьмет текущий destination (который сейчас отображается в контейнере, т.е. destination fragment1)
2) найдет у него action с ID = action_fragment1_to_fragment2
3) определит, что этот action ведет в destination fragment2
4) определит, что destination fragment2 - это фрагмент Fragment2
5) отобразит Fragment2 и при этом применит параметры, которые были заданы в action_fragment1_to_fragment2
Если мы попытаемся вызвать action не находясь в destination, которому этот action принадлежит, то будет ошибка. Т.е. action action_fragment1_to_fragment2 мы можем вызывать только находясь в destination fragment1, потому что при создании action мы рисовали его из destination fragment1.
Из одного destination можно создать несколько action:
Activity
В качестве destination мы можем использовать не только фрагменты, но и Activity.
В этом же примере я создал SecondActivity и фрагменты Fragment4 и Fragment5. Будем вызывать их из Fragment3, находящемся в MainActivity.
Открываем граф main_graph и добавляем SecondActivity, как новый destination.
destination создан, его ID = secondActivity
По нажатию на кнопку Next в Fragment3 будем вызывать этот destination
@Override public void onFragment3NextClick() { navController.navigate(R.id.secondActivity); }
Контроллер найдет в графе, что destination с таким ID соответствует SecondActivity и запустит его.
На этом полномочия графа main_graph заканчиваются. В новом Activity нам нужен новый граф.
Создаем second_graph и добавляем туда Fragment4 и Fragment5
Fragment4 - стартовый, он будет отображен при открытии SecondActivity.
В layout activity_second добавляем контейнер NavHostFragment и указываем граф second_graph
<?xml version="1.0" encoding="utf-8"?> <FrameLayout 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=".SecondActivity"> <fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/second_graph" /> </FrameLayout>
В SecondActivity находим контроллер и в обработчиках нажатий кнопок фрагментов используем его для навигации.
NavController navController; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); navController = Navigation.findNavController(this, R.id.nav_host_fragment); } @Override public void onFragment4NextClick() { navController.navigate(R.id.fragment5); } @Override public void onFragment4BackClick() {} @Override public void onFragment5NextClick() {} @Override public void onFragment5BackClick() { navController.popBackStack(); }
Запускаем приложение
Из фрагмента Fragment3 переходим в SecondActivity. Там сразу открывается Fragment4, т.к. он стартовый. Из него переходим в Fragment5 и обратно. А вот возвращаться из SecondActivity в MainActivity приходится с помощью системной кнопки Back. Контроллер в SecondActivity работает только в пределах этого Activity. Он ничего не знает за его пределами. Он не знает, что делать, когда вызывается popBackStack в стартовом фрагменте, т.е. в Fragment4. Тут уже нам надо самим. Например, можно в onFragment4BackClick вызывать метод finish, чтобы закрыть Activity.
Метод navController.popBackStack возвращает boolean. Если контроллер сам смог вернуться на шаг назад, то он вернет true. Если же он не знает, что делать, то вернет false и в этом случае мы сами можем обработать эту ситуацию.
destination
Напоследок пару слов о понятии destination в Navigation Component. В грАфе у каждого destination есть ID, и мы указываем этот ID в методе navigate, когда просим контроллер открыть destination. При этом нам не важно, чем является в графе этот destination - Activity или фрагментом. Это забота контролера. Он сам это определит и вызовет либо startActivity, либо будет работать с FragmentManager.
Например, у нас в приложении есть экран конфигурации. Это фрагмент ConfigFragment. В графе у нас этот фрагмент фигурирует как destination с ID = configScreen. И мы открываем его вызовом метода navController.navigate(R.id.configScreen).
Внезапно мы решаем, что надо экран конфигурации вынести в отдельное ConfigActivity. Создаем это Activity, переносим все туда и добавляем его в граф вместо ConfigFragment, под тем же ID = configScreen.
При этом в приложении вообще никак не меняется код вызова экрана конфигурации. Это так и остается вызов метода navigate с указанием ID = configScreen. Но теперь контроллер будет открывать не ConfigFragment в текущем контейнере, а запустит новое Activity, потому что мы настроили это в графе.
А вот так выглядит содержимое графа main_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" app:startDestination="@id/fragment1"> <fragment android:id="@+id/fragment1" android:name="ru.startandroid.navigation.Fragment1" android:label="fragment1" tools:layout="@layout/fragment1" > <action android:id="@+id/action_fragment1_to_fragment2" app:destination="@id/fragment2" /> </fragment> <fragment android:id="@+id/fragment2" android:name="ru.startandroid.navigation.Fragment2" android:label="fragment2" tools:layout="@layout/fragment2" /> <fragment android:id="@+id/fragment3" android:name="ru.startandroid.navigation.Fragment3" android:label="fragment3" tools:layout="@layout/fragment3" /> <activity android:id="@+id/secondActivity" android:name="ru.startandroid.navigation.SecondActivity" android:label="activity_second" tools:layout="@layout/activity_second" /> </navigation>
Три фрагмента, одно Activity и у первого фрагмента есть action, который ведет во второй фрагмент.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня