はじめに
AndroidでBottomNavigationViewとNavigationを併用する場合、デフォルトでタブ切り替え時にFragmentが再生成されます。
よって、再生成されないようにするためにはFragmentの状態保持を実装する必要があります。
この実装方法について調べた際、最新バージョンのNavigationを使った実装方法にたどり着けなく、苦労したのでまとめます。
言語はKotlinでバージョンは1.3.31です。
導入
// ...
dependencies {
// ...
// Navigation
def arch_navigation_version = '2.2.0-alpha01'
implementation "androidx.navigation:navigation-fragment:$arch_navigation_version"
implementation "androidx.navigation:navigation-fragment-ktx:$arch_navigation_version"
implementation "androidx.navigation:navigation-ui:$arch_navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$arch_navigation_version"
// Material
implementation 'com.google.android.material:material:1.0.0'
}
FragmentNavigatorクラスのnavigate()を上書き
Navigationライブラリに実装されているFragmentNavigator
クラスのnavigate()
でタブ切り替え時にFragmentが再生成されるようになっています。
よって、FragmentNavigator
クラスを継承したCustomNavigator
クラスを作成し、状態保持されるようにnavigate()
を上書きます。
アニメーション周りの実装とBackStack周りの実装を排除し、状態保持したタブ切り替えのみを行うnavigate()
が以下です。
@Navigator.Name("custom_fragment")
class CustomNavigator(
private val context: Context,
private val manager: FragmentManager,
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
@Suppress("DEPRECATION")
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
if (manager.isStateSaved) {
return null
}
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.hide(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = instantiateFragment(context, manager, className, args)
transaction.add(containerId, fragment, tag)
}
fragment.arguments = args
transaction.show(fragment)
transaction.setPrimaryNavigationFragment(fragment)
transaction.commit()
return destination
}
}
NavHostFragmentクラスのcreateFragmentNavigator()を上書き
次に、Navigationライブラリに実装されているNavHostFragment
クラスのcreateFragmentNavigator()
を上書きます。FragmentNavigator
クラスのインスタンスが戻り値になっているためです。
NavHostFragment
を継承したCustomNavHostFragment
を作成し、上記のCustomNavigator
クラスのインスタンスが戻り値になるようにcreateFragmentNavigator()
を上書きます。
class CustomNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> {
return CustomNavigator(requireContext(), childFragmentManager, id)
}
}
navigationレイアウトの作成
CustomNavigator
クラスにつけた@Navigator.Name
アノテーションの引数に指定しているcustom_fragment
を利用します。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bottom_navigation"
app:startDestination="@+id/tab1">
<custom_fragment
android:id="@+id/tab1"
android:name="hoge.HogeFragment"
android:label="tab1"
android:tag="tab1" />
<custom_fragment
android:id="@+id/tab2"
android:name="hoge.HogeHogeFragment"
android:label="tab2"
android:tag="tab2" />
</navigation>
fragmentとBottomNavigationViewをレイアウトに作成
BottomNavigationViewとFragmentを表示したいレイアウトにBottomNavigationView
とfragment
を記載します。
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="hoge.MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.appcompat.widget.Toolbar>
<fragment
android:id="@+id/bottom_navigation_view_fragment"
android:name="hoge.navigation.CustomNavHostFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:navGraph="@navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_navigation_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
以下が注意点です
- fragment
-
android:name
にCustomNavHostFragment
を指定 -
app:navGraph
に上記で記載したnavigationレイアウトを指定
-
- BottomNavigationView
-
app:menu
に指定するmenuレイアウトのitem
のandroid:id
をcustom_fragment
のandroid:id
と揃えること
-