В моем приложении пользователь добавляет клиентов, консультации и расходы. Для всех трех типов данных в нем свой фрагмент, список 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"/>

Итоги

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

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