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

в 13:40, , рубрики: keep state navigator, kotlin, кастомный навигатор нижнего меню, нижнее меню, Разработка под android, сохранение состояния, фрагменты

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

Итоги

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

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

Автор:
Andrey_Ananiev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js