android studio. Kotlin. Custom bottom menu navigator. Keep state navigator

In my application, the user adds clients, consultations, and expenses. For all three data types, it has its own fragment, a RecyclerView list and a bottom menu for switching between them. I decided to make sure that when the fragment is changed, the state of each of them is saved, and the user can return to the line of the list on which he was after switching from another fragment. It turned out to be possible to do this (correct me in the comments if this is not so) only if you write your custom bottom menu navigator, which, when switching between fragments, will save the state of each of them. This article describes how I did it.

As it was. Standard bottom menu navigator

I think it’s worth bringing the code as it was before I made changes and connected a custom navigator. This is how the fragment of the onCreate function in MainActivity, connecting the bottom menu, looked like:

...

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

...

For a better understanding, I will also give a fragment of the activity_main.xml code, as it was before the changes made:

<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"/>

The menu for the navigator (bottom_nav_menu) looked like this:

<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>

And the navigation graph (navigation_graph) is like this:

<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>

What was done. Connecting a custom navigator

1. KeepStateNavigator class

I found the following code somewhere on the net, still not really understanding how it works. It overrides the navigate function, which is responsible for switching screen fragments when the user clicks on the bottom menu.

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
        }
    }
}

Notice this line of code: @Navigator.Name(“keep_state_fragment”) This sets the name of the navigation element instead of “fragment”.

2. Changes in 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>

I change “fragment” to “keep_state_fragment”, I don’t touch anything else.

3. Changes in the function 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)

...

There are two things to note in this code. First, in line 14, we add a custom navigator to the standard one, and do not replace it with it (navController.navigatorProvider += navigator). Secondly, the navigation graph is now set in code, and not in XML, as before (navController.setGraph(R.navigation.navigation_graph)).

4. The final touch, but without which nothing works

I almost gave up using the custom bottom menu navigator in my fragment due to the fact that it flatly refused to work. It is mandatory to remove the line “app:navGraph=”@navigation/navigation_graph”” from 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"/>

Results

It seems that he did not forget to specify anything in the description of connecting the custom navigator of the lower menu. Please do not throw stones at me for not describing in detail his work. I don’t really understand myself. I do programming as a hobby. I will be glad to your comments. And I hope this guide will be useful to someone.

The application I am currently working on – Accounting for self-employed clients – is available at the link: https://play.google.com/store/apps/details?id=ru.keytomyself.customeraccounting

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *