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