Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В моем приложении пользователь добавляет клиентов, консультации и расходы. Для всех трех типов данных в нем свой фрагмент, список 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"/>
Итоги
Вроде бы ничего не забыл указать в описании подключения кастомного навигатора нижнего меню. Прошу не кидать в меня камнями за то, что не описываю в подробностях его работу. Сам не очень понимаю. Занимаюсь программированием в качестве хобби. Буду рад вашим комментариям. И надеюсь, кому-нибудь этот гайд будет полезен.
Приложение над которым я сейчас работаю - Учет клиентов для самозанятых - доступно по ссылке: https://play.google.com/store/apps/details?id=ru.keytomyself.customeraccounting