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

Итоги

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

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

Комментарии (8)


  1. qoj
    26.02.2022 17:35
    +1

    Буквально пару недель назад вышел стабильный navigation component 2.4.0, который поддерживает multiple backstack из коробки.


    1. Andrey_Ananiev Автор
      26.02.2022 17:46

      Спасибо! Опять я изобретаю велосипед... Видимо, это моя участь.


      1. Andrey_Ananiev Автор
        26.02.2022 17:55

        Вот, что пишет, когда я повышаю версию navigation component:

        Dependency 'androidx.navigation:navigation-fragment-ktx:2.4.1' requires 'compileSdkVersion' to be set to 31 or higher.
        Compilation target for module ':app' is 'android-30'


        1. qoj
          26.02.2022 19:46

          Просит поднять compileSdk в build.gradle. С этим не должно быть проблем, тем более с 30 до 31.


          1. Andrey_Ananiev Автор
            26.02.2022 23:08

            Тогда ведь приложение перестанет работать на предыдущих версиях Андроид?


            1. italankin
              27.02.2022 10:18

              Это вы говорите про minSdk, а с поднятием compileSdk проблем не будет.


            1. kavaynya
              28.02.2022 11:52

              Это compileSdk, а не minSdk, который и ограничивает минимальную версию для запуска