Эта статья не пошаговая инструкция, в ней опущены детали реализации, чтобы сосредоточить внимание на ключевых моментах. В интернете есть немало
Сразу скажу, библиотеку безусловно считаю полезной и не исключаю возможности неверного использования, но, пожалуй, я перепробовал всё прежде чем писать эту статью.
Итак, вот сценарии, при реализации которых ожидания по функционалу не совпали с реальностью в реализации:
- переключение между пунктами меню в navigation drawer
- открытие новой Activity со своим графом навигации
- передача параметров в startDestination
Переключение между пунктами меню
Это одна из тех функций, которые повлияли на решение использовать Navigation Component.
Нужно всего лишь сделать одинаковыми id пунктов меню
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="@+id/importFragment"
android:icon="@drawable/ic_menu_camera"
android:title="Import"/>
<item
android:id="@+id/galleryFragment"
android:icon="@drawable/ic_menu_gallery"
android:title="Gallery"/>
<item
android:id="@+id/slideshowFragment"
android:icon="@drawable/ic_menu_slideshow"
android:title="Slideshow"/>
<!-- Остальная часть меню -->
и id экранов (destination в графе навигации)
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/mobile_navigation"
app:startDestination="@id/importFragment">
<fragment
android:id="@+id/importFragment"
android:name="com.xiii.navigationapplication.ImportFragment"
android:label="fragment_import"
tools:layout="@layout/fragment_import"/>
<fragment
android:id="@+id/galleryFragment"
android:name="com.xiii.navigationapplication.GalleryFragment"
android:label="fragment_gallery"
tools:layout="@layout/fragment_gallery"/>
<fragment
android:id="@+id/slideshowFragment"
android:name="com.xiii.navigationapplication.SlideshowFragment"
android:label="fragment_slideshow"
tools:layout="@layout/fragment_slideshow"/>
</navigation>
затем нужно связать меню с контроллером навигации:
class MainActivity : AppCompatActivity() {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.nav_host_fragment)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// предоставим управление "гамбургером" в toolbar
NavigationUI.setupWithNavController(toolbar, navController, drawer_layout)
// связывание контроллера навигации и меню
nav_view.setupWithNavController(navController)
}
// добавим поддержку кнопки назад
override fun onSupportNavigateUp() = navController.navigateUp()
}
Навигация в меню заработала — ну разве не чудо?!
Обратите внимание на «гамбургер» (иконка меню), при переключении между пунктами меню он меняет своё состояние на кнопку «назад». Такое поведение показалось непривычным (привычное — как в приложении play market) и, какое-то время, я пытался разобраться, что же сделал не так?
Всё так! Перечитав документацию по принципам навигации (а именно: пункты два и три), понял, что «гамбургер» показывается только для startDestination, вернее так: кнопка «назад» показывается для всех, кроме startDestination. Ситуацию можно поменять применив различные уловки в подписке (addOnNavigatedListener()) на изменение destination, но их даже описывать не стоит. Работает так, нужно смириться.
Открытие новой Activity
Activity может выступать в качестве navigation host и, в то же время, в графе навигации может выступать в роли одного из destination. Открытие Activity без вложенного графа навигации работает как ожидается, то есть вызов:
navController.navigate(R.id.editActivity)
осуществит переход (как в случае с фрагментами) и откроет запрошенную Activity.
Гораздо интереснее рассмотреть случай, когда целевая Activity сама выступает в роли navigation host, то есть вариант 2 из документации:
В качестве примера давайте рассмотрим Activity для добавления заметки. В ней будет основной фрагмент с полями ввода EditFragment, он в графе навигации будет startDestination. Давайте положим, что при редактировании нам нужно прикрепить фото, для этого будем переходить к PhotoFragment для получения снимка с камеры. Граф навигации будет выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/edit_navigation"
app:startDestination="@id/editFragment">
<fragment
android:id="@+id/editFragment"
android:name="com.xiii.navigationapplication.ui.edit.EditFragment"
android:label="fragment_edit"
tools:layout="@layout/fragment_edit">
<action
android:id="@+id/action_editFragment_to_photoFragment"
app:destination="@id/photoFragment"/>
</fragment>
<fragment
android:id="@+id/photoFragment"
android:name="com.xiii.navigationapplication.ui.edit.PhotoFragment"
android:label="fragment_photo"
tools:layout="@layout/fragment_photo"/>
</navigation>
EditActivity мало отличается от MainActivity. Основное отличие в том, что на EditActivity нет меню:
class EditActivity : AppCompatActivity() {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.nav_host_fragment)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit)
setSupportActionBar(toolbar)
// предоставим управление кнопкой "Назад" в toolbar
NavigationUI.setupWithNavController(toolbar, navController)
}
override fun onSupportNavigateUp() = navController.navigateUp()
fun takePhoto(view: View) {
navController.navigate(R.id.action_editFragment_to_photoFragment)
}
}
Activity открывается, навигация внутри неё работает:
Опять обратим внимание на кнопку навигации в toolbar — на стартовом EditFragment нет кнопки «Назад к parent Activity» (а хотелось бы). С точки зрения документации, тут всё законно: новый граф навигации, новое значение startDestination, на startDestination не показывается кнопка «Назад», конец.
Для тех, кому хочется вернуть привычное поведение c parent activity, сохранив при этом функционал переключения между фрагментами, могу предложить такой
<activity android:name=".EditActivity"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<!-- Parent activity meta-data to support 4.0 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
class EditActivity : AppCompatActivity() {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.nav_host_fragment)
}
private var isStartDestination = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit)
setSupportActionBar(toolbar)
val startDestinationId = navController.graph.startDestination
// будем принудительно подменять id, чтобы NavigationUI.ActionBarOnNavigatedListener не пытался скрыть кнопку
// при этом для всех остальных destination будем предоставлять оригинальный startDestination
navController.addOnNavigatedListener { controller, destination ->
isStartDestination = destination.id == startDestinationId
// R.id.fake_start_destination этот id вручную определён для семантики и обеспечения уникальности
controller.graph.startDestination = if (isStartDestination) R.id.fake_start_destination else startDestinationId
}
// предоставим управление кнопкой "Назад" в toolbar
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
// на startDestination не будем использовать навигацию от Navigation Component
return if (isStartDestination) super.onSupportNavigateUp() else navController.navigateUp()
}
fun takePhoto(view: View) {
navController.navigate(R.id.action_editFragment_to_photoFragment)
}
}
Подписка нужна для того, чтобы для NavigationUI.ActionBarOnNavigatedListener все destination не являлись startDestination. Таким образом NavigationUI.ActionBarOnNavigatedListener не будет скрывать кнопку навигации (за деталями стоит обратиться к исходникам). Добавим к этому обработку onSupportNavigateUp() штатным образом на startDestination и получим то, что хотелось.
Стоит сказать, что решение это далеко от идеала хотя бы потому, что это неочевидное вмешательство в поведение библиотеки. Полагаю, могут возникнуть проблемы в случае использования deep links (ещё не проверял).
Передача параметров в startDestination
В Navigation Component есть механизм передачи параметров от одного destination другому. Есть даже инструмент для обеспечения безопасности типов за счёт кодогенерации (неплохо).
Сейчас мы разберём случай, из-за которого я не смог поставить твёрдую пятёрку этому функционалу.
Вернёмся к EditActivity, достаточно привычный сценарий, когда одна Activity используется для создания и редактирования объектов. При открытии объекта для редактирования в Activity нужно передать, например, id объекта — давайте сделаем это штатным образом:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/edit_navigation"
app:startDestination="@id/editFragment">
<argument
android:name="id"
app:argType="integer"/>
<fragment
android:id="@+id/editFragment"
android:name="com.xiii.navigationapplication.ui.edit.EditFragment"
android:label="fragment_edit"
tools:layout="@layout/fragment_edit">
<action
android:id="@+id/action_editFragment_to_photoFragment"
app:destination="@id/photoFragment"/>
</fragment>
<fragment
android:id="@+id/photoFragment"
android:name="com.xiii.navigationapplication.ui.edit.PhotoFragment"
android:label="fragment_photo"
tools:layout="@layout/fragment_photo"/>
</navigation>
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/mobile_navigation"
app:startDestination="@id/importFragment">
<fragment
android:id="@+id/importFragment"
android:name="com.xiii.navigationapplication.ImportFragment"
android:label="fragment_import"
tools:layout="@layout/fragment_import">
<action
android:id="@+id/add"
app:destination="@id/editActivity">
<argument
android:name="id"
app:argType="integer"
android:defaultValue="0"/>
</action>
<action
android:id="@+id/edit"
app:destination="@id/editActivity">
<argument
android:name="id"
app:argType="integer"/>
</action>
</fragment>
<fragment
android:id="@+id/galleryFragment"
android:name="com.xiii.navigationapplication.GalleryFragment"
android:label="fragment_gallery"
tools:layout="@layout/fragment_gallery"/>
<fragment
android:id="@+id/slideshowFragment"
android:name="com.xiii.navigationapplication.SlideshowFragment"
android:label="fragment_slideshow"
tools:layout="@layout/fragment_slideshow"/>
<activity
android:id="@+id/editActivity"
android:name="com.xiii.navigationapplication.EditActivity"
android:label="activity_edit"
tools:layout="@layout/activity_edit"/>
</navigation>
val direction = ImportFragmentDirections.edit(123 /* id заметки */)
navController.navigate(direction)
class EditFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// работает во остальных случаях, когда fragment не создаётся в качестве startDestination
// val id = EditFragmentArgs.fromBundle(arguments)
val id = EditFragmentArgs.fromBundle(requireActivity().intent.extras)
return inflater.inflate(R.layout.fragment_edit, container, false)
}
}
Вы, наверняка, обратили внимание на особенности получения параметров в EditFragment. Так работает, потому что edit action (из пункта 1) передаёт аргументы в EditActivity, а она, в свою очередь, почему-то
Пожалуй, наибольшая сложность возникнет при одновременном использовании в качестве startDestination и обычного destination. То есть, при переходе и передаче параметров в startDestination из любого другого destination этого графа, фрагменту придётся самостоятельно определять, откуда извлекать параметры: из arguments или из intent.extras. Это нужно иметь ввиду при проектировании переходов с передачей параметров.
Резюмируя, хочу отметить, что сам не перестал использовать библиотеку и, несмотря на перечисленные
Благодарю за внимание. Рабочего Вам кода!
Исходники для статьи размещены на GitHub.
Комментарии (11)
Logos_Intellect
08.11.2018 20:20Google, как бы, намекает на использование подхода Single Activity
XIII-th Автор
08.11.2018 20:51Это верно, но мне хотелось разделить логику просмотра и редактирования, чтобы не перегружать одну Activity элементами, которые нужны «не всем».
Рассмотрим, к примеру, toolbar — это общий компонент для всех фрагментов. Одни фрагменты
(фрагменты списков) захотят видеть в нём поиск, другим нужно будет показывать сжимающуюся картинку (инфо фрагменты), а третьим и вовсе захочется встроить в toolbar поле ввода (фрагменты редактирования). Реализовав всё это в одной activity, мы получим монстр-toolbar/actvity со всеми вытекающими последствиями.
Такой подход имеет право на жизнь, но мне привычнее жить с разделением ответственности.Logos_Intellect
09.11.2018 03:13Так у каждого фрагмента свой toolbar в xml файле прописываешь. Я обычно делаю общее только DrawerLayout или BottomNavigationBar. Ну и естественно всякие алерты вроде «Нет подключения к интернету»
egordeev
08.11.2018 20:59вроде фрагменты были придуманы как workaround?
XIII-th Автор
09.11.2018 15:58Думаю, это не так. В графе навигации вместе неплохо уживаются и фрагменты и Activity. Более того, документация заявляет, что можно без особого труда реализовать свой узел навигации, хотя описано довольно скудно.
barbanel
Отдельное спасибо за КДПВ, порадовали =)
XIII-th Автор
Рад, что удалось отразить основную мысль )