Android Studio. Kotlin. Кастомный навигатор нижнего меню. Keep state navigator

от автора

В моем приложении пользователь добавляет клиентов, консультации и расходы. Для всех трех типов данных в нем свой фрагмент, список RecyclerView и нижнее меню для перехода между ними. Я решил сделать так, чтобы при смене фрагмента состояние каждого из них сохранялось, и пользователь смог бы вернуться к той строке списка, на которой он был после перехода с другого фрагмента. Сделать это оказалось возможным (поправьте меня в комментариях, если это не так) только, если написать свой кастомный навигатор нижнего меню, который при переключении между фрагментами будет сохранять состояние каждого из них. В этой статье описываю то, как я это сделал.

Как было. Стандартный навигатор нижнего меню

Думаю стоит привести код, какой он был до внесения мной изменений и подключения кастомного навигатора. Вот так выглядел фрагмент функции onCreate в MainActivity, подключающий нижнее меню:

...  val navController = findNavController(R.id.nav_host_fragment)   val navView = findViewById<BottomNavigationView>(R.id.nav_view) navView.setupWithNavController(navController)  ...

Для лучшего понимания приведу также фрагмент кода activity_main.xml, как оно было до внесенных изменений:

<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/navigation_graph"/>  <com.google.android.material.bottomnavigation.BottomNavigationView         android:id="@+id/nav_view"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_gravity="bottom"         app:menu="@menu/bottom_nav_menu"/>

Меню для навигатора (bottom_nav_menu) выглядело так:

<menu xmlns:android="http://schemas.android.com/apk/res/android">      <item         android:id="@+id/navigation_clients"         android:icon="@drawable/ic_clients"         android:title="@string/title_clients" />      <item         android:id="@+id/navigation_services"         android:icon="@drawable/ic_timetable"         android:title="@string/title_services" />      <item         android:id="@+id/navigation_expenses"         android:icon="@drawable/ic_expenses"         android:title="@string/title_expenses" />      <item         android:id="@+id/navigation_analytics"         android:icon="@drawable/ic_analytics"         android:title="@string/title_analytics" />  </menu>

А навигационный граф (navigation_graph) так:

<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/navigation_clients">      <fragment         android:id="@+id/navigation_clients"         android:name="ru.keytomyself.customeraccounting.fragments.ClientsFragment"         android:label="@string/title_clients"         tools:layout="@layout/fragment_clients" />      <fragment         android:id="@+id/navigation_services"         android:name="ru.keytomyself.customeraccounting.fragments.ServiceFragment"         android:label="@string/title_services"         tools:layout="@layout/fragment_services" />      <fragment         android:id="@+id/navigation_expenses"         android:name="ru.keytomyself.customeraccounting.fragments.ExpensesFragment"         android:label="@string/title_expenses"         tools:layout="@layout/fragment_expenses" />      <fragment         android:id="@+id/navigation_analytics"         android:name="ru.keytomyself.customeraccounting.fragments.AnalyticsFragment"         android:label="@string/title_analytics"         tools:layout="@layout/fragment_analytics" /> </navigation>

Что было сделано. Подключение кастомного навигатора

1. Класс KeepStateNavigator

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

package ru.keytomyself.customeraccounting  import android.content.Context import android.os.Bundle import androidx.fragment.app.FragmentManager import androidx.navigation.NavDestination import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.fragment.FragmentNavigator  @Navigator.Name("keep_state_fragment") class KeepStateNavigator(     private val context: Context,     private val manager: FragmentManager,     private val containerId: Int ) : FragmentNavigator(context, manager, containerId) {      override fun navigate(         destination: Destination,         args: Bundle?,         navOptions: NavOptions?,         navigatorExtras: Navigator.Extras?     ): NavDestination? {          val tag = destination.id.toString()         val transaction = manager.beginTransaction()          var initialNavigate = false         val currentFragment = manager.primaryNavigationFragment         if (currentFragment != null) {             transaction.detach(currentFragment)         } else {             initialNavigate = true         }          var fragment = manager.findFragmentByTag(tag)         if (fragment == null) {             val className = destination.className             fragment = manager.fragmentFactory.instantiate(context.classLoader, className)             transaction.add(containerId, fragment, tag)         } else {             transaction.attach(fragment)         }          transaction.setPrimaryNavigationFragment(fragment)         transaction.setReorderingAllowed(true)         transaction.commitNow()                   return if (initialNavigate) {             destination         } else {             null         }     } }

Обратите внимание на эту строчку кода: @Navigator.Name(«keep_state_fragment») Здесь задается название элемента навигации вместо «fragment».

2. Изменения в navigation_graph

<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/navigation_clients">      <keep_state_fragment         android:id="@+id/navigation_clients"         android:name="ru.keytomyself.customeraccounting.fragments.ClientsFragment"         android:label="@string/title_clients"         tools:layout="@layout/fragment_clients" />      <keep_state_fragment         android:id="@+id/navigation_services"         android:name="ru.keytomyself.customeraccounting.fragments.ServiceFragment"         android:label="@string/title_services"         tools:layout="@layout/fragment_services" />      <keep_state_fragment         android:id="@+id/navigation_expenses"         android:name="ru.keytomyself.customeraccounting.fragments.ExpensesFragment"         android:label="@string/title_expenses"         tools:layout="@layout/fragment_expenses" />      <keep_state_fragment         android:id="@+id/navigation_analytics"         android:name="ru.keytomyself.customeraccounting.fragments.AnalyticsFragment"         android:label="@string/title_analytics"         tools:layout="@layout/fragment_analytics" /> </navigation>

Меняю «fragment» на «keep_state_fragment», больше ничего не трогаю.

3. Изменения в функции onCreate MainActivity

...  val navController = findNavController(R.id.nav_host_fragment)  // получаем фрагмент val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!  // устанавливаем кастомный навигатор val navigator = KeepStateNavigator(     this,     navHostFragment.childFragmentManager,     R.id.nav_host_fragment ) navController.navigatorProvider += navigator  // устанавливаем navigation graph navController.setGraph(R.navigation.navigation_graph)  val navView = findViewById<BottomNavigationView>(R.id.nav_view) navView.setupWithNavController(navController)  ...

В этом коде стоит обратить внимание на две вещи. Во-первых, в 14 строке мы кастомный навигатор добавляем к стандартному, а не заменяем им его (navController.navigatorProvider += navigator). Во-вторых, navigation graph устанавливаем теперь в коде, а не в XML, как раньше (navController.setGraph(R.navigation.navigation_graph)).

4. Последний штрих, но без которого ничего не работает

Я уже почти отказался от использования кастомного навигатора нижнего меню в своем фрагменте из-за того, что он наотрез отказывался работать. В обязательном порядке необходимо удалить строчку «app:navGraph=»@navigation/navigation_graph»» из activity_main.xml

<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"/>  <com.google.android.material.bottomnavigation.BottomNavigationView         android:id="@+id/nav_view"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_gravity="bottom"         app:menu="@menu/bottom_nav_menu"/>

Итоги

Вроде бы ничего не забыл указать в описании подключения кастомного навигатора нижнего меню. Прошу не кидать в меня камнями за то, что не описываю в подробностях его работу. Сам не очень понимаю. Занимаюсь программированием в качестве хобби. Буду рад вашим комментариям. И надеюсь, кому-нибудь этот гайд будет полезен.

Приложение над которым я сейчас работаю — Учет клиентов для самозанятых — доступно по ссылке.


ссылка на оригинал статьи https://habr.com/ru/post/653577/