将 BottomNavigationView 与新的 NavController 一起使用时,有没有办法让片段保持活动状态?
Is there a way to keep fragment alive when using BottomNavigationView with new NavController?
我正在尝试使用新的导航组件。我将 BottomNavigationView 与 navController 一起使用:NavigationUI.setupWithNavController(bottomNavigation, navController)
但是当我切换片段时,它们每次都是 destroy/create 即使它们以前被使用过。
有没有办法让我们的主要片段 link 保持在我们的 BottomNavigationView 中?
试试这个。
导航器
创建自定义导航器。
@Navigator.Name("custom_fragment") // Use as custom tag at navigation.xml
class CustomNavigator(
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?) {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = destination.createFragment(args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
}
}
NavHostFragment
创建自定义 NavHostFragment。
class CustomNavHostFragment: NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
}
}
navigation.xml
使用自定义标签而不是片段标签。
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation"
app:startDestination="@id/navigation_first">
<custom_fragment
android:id="@+id/navigation_first"
android:name="com.example.sample.FirstFragment"
android:label="FirstFragment" />
<custom_fragment
android:id="@+id/navigation_second"
android:name="com.example.sample.SecondFragment"
android:label="SecondFragment" />
</navigation>
activity布局
使用 CustomNavHostFragment 而不是 NavHostFragment。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="com.example.sample.CustomNavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
更新
我创建了示例项目。 link
我不创建自定义 NavHostFragment。我用 navController.navigatorProvider += navigator
.
目前不可用。
作为解决方法,您可以将所有获取的数据存储到视图模型中,并在您重新创建片段时让这些数据随时可用。确保使用活动上下文获取视图。
您可以使用 LiveData 让您的数据生命周期感知可观察
更新 2021 年 5 月 19 日多个后台堆栈
从 Jetpack Navigation 2.4.0-alpha01 开始,我们开箱即用。
检查 Google Navigation Adavanced Sample
旧答案:
Google samples link
只需将 NavigationExtensions 复制到您的应用程序并通过示例进行配置。效果很好。
经过数小时的研究,我找到了解决方案。它一直就在我们面前 :) 有一个函数:popBackStack(destination, inclusive)
如果在 backStack 中找到,它会导航到给定的目的地。它 returns 布尔值,因此如果控制器找不到片段,我们可以手动导航到那里。
if(findNavController().popBackStack(R.id.settingsFragment, false)) {
Log.d(TAG, "SettingsFragment found in backStack")
} else {
Log.d(TAG, "SettingsFragment not found in backStack, navigate manually")
findNavController().navigate(R.id.settingsFragment)
}
我用过@STAR_ZERO提供的link,效果很好。对于后退按钮有问题的人,您可以像这样在 activity / nav 主机中处理它。
override fun onBackPressed() {
if(navController.currentDestination!!.id!=R.id.homeFragment){
navController.navigate(R.id.homeFragment)
}else{
super.onBackPressed()
}
}
只需检查当前目的地是否是您的根/主页片段(通常是底部导航视图中的第一个片段),如果不是,则导航回该片段,如果是,则仅退出应用程序或执行任何您想做的事情。
顺便说一句,此解决方案需要与 STAR_ZERO 提供的上述解决方案 link 一起使用,使用 keep_state_fragment.
@piotr-prus 提供的解决方案帮助了我,但我不得不添加一些当前目的地检查:
if (navController.currentDestination?.id == resId) {
return //do not navigate
}
如果您错误地导航到当前目的地,则如果不进行此检查,将会重新创建当前目的地,因为它不会在返回堆栈中找到。
如果您在传递参数时遇到问题,请添加:
fragment.arguments = args
在 class KeepStateNavigator
如果您在这里只是为了在使用 BottomNavigationView
和 NavController
的片段之间导航时保持 精确 RecyclerView
滚动状态,那么那里一种简单的方法是将 layoutManager
状态存储在 onDestroyView
并在 onCreateView
上恢复它
我使用 ActivityViewModel 来存储状态。如果您使用不同的方法,请确保将状态存储在父 activity 或任何比片段本身存活时间更长的东西中。
片段
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerview.adapter = MyAdapter()
activityViewModel.listStateParcel?.let { parcelable ->
recyclerview.layoutManager?.onRestoreInstanceState(parcelable)
activityViewModel.listStateParcel = null
}
}
override fun onDestroyView() {
val listState = planet_list?.layoutManager?.onSaveInstanceState()
listState?.let { activityViewModel.saveListState(it) }
super.onDestroyView()
}
视图模型
var plantListStateParcel: Parcelable? = null
fun savePlanetListState(parcel: Parcelable) {
plantListStateParcel = parcel
}
在最新的导航组件版本中 - 底部导航视图将跟踪堆栈中的最新片段。
这是一个示例:
https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample
示例代码
在项目中 build.gradle
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha01"
}
在应用程序中 build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs'
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.4.0-alpha01"
}
在您的 activity 中 - 您可以使用 toolbar
和 bottom navigation view
设置导航
val navHostFragment = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
//setup with bottom navigation view
binding.bottomNavigationView.setupWithNavController(navController)
//if you want to disable back icon in first page of the bottom navigation view
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.feedFragment,
R.id.favoriteFragment
)
).
//setup with toolbar back navigation
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
现在在您的片段中,您可以导航到第二个片段,当您 deselect/select 底部导航项时 - NavController 会记住堆栈中的最后一个片段。
示例:在您的自定义适配器中
adapter.setOnItemClickListener { item ->
findNavController().navigate(
R.id.action_Fragment1_to_Fragment2
)
}
现在,当您在 fragment 2 中按回键时,NavController 会自动弹出 fragment 1。
https://developer.android.com/guide/navigation/navigation-navigate
自定义通用片段导航的超级简单解决方案:
第 1 步
创建 FragmentNavigator
的子类,根据需要覆盖 instantiateFragment
或 navigate
。如果我们只想创建一次片段,我们可以在这里缓存它,return 在 instantiateFragment
方法中缓存一个。
第 2 步
创建 NavHostFragment
的子类,覆盖 createFragmentNavigator
或 onCreateNavController
,以便可以注入我们自定义的导航器(在步骤 1 中)。
第 3 步
将 android:name="com.example.learn1.navigation.TabNavHostFragment"
中的布局 xml FragmentContainerView
标记属性替换为您自定义的 navHostFragment(在第 2 步中)。
我正在尝试使用新的导航组件。我将 BottomNavigationView 与 navController 一起使用:NavigationUI.setupWithNavController(bottomNavigation, navController)
但是当我切换片段时,它们每次都是 destroy/create 即使它们以前被使用过。
有没有办法让我们的主要片段 link 保持在我们的 BottomNavigationView 中?
试试这个。
导航器
创建自定义导航器。
@Navigator.Name("custom_fragment") // Use as custom tag at navigation.xml
class CustomNavigator(
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?) {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = destination.createFragment(args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
}
}
NavHostFragment
创建自定义 NavHostFragment。
class CustomNavHostFragment: NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
}
}
navigation.xml
使用自定义标签而不是片段标签。
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation"
app:startDestination="@id/navigation_first">
<custom_fragment
android:id="@+id/navigation_first"
android:name="com.example.sample.FirstFragment"
android:label="FirstFragment" />
<custom_fragment
android:id="@+id/navigation_second"
android:name="com.example.sample.SecondFragment"
android:label="SecondFragment" />
</navigation>
activity布局
使用 CustomNavHostFragment 而不是 NavHostFragment。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="com.example.sample.CustomNavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
更新
我创建了示例项目。 link
我不创建自定义 NavHostFragment。我用 navController.navigatorProvider += navigator
.
目前不可用。
作为解决方法,您可以将所有获取的数据存储到视图模型中,并在您重新创建片段时让这些数据随时可用。确保使用活动上下文获取视图。
您可以使用 LiveData 让您的数据生命周期感知可观察
更新 2021 年 5 月 19 日多个后台堆栈
从 Jetpack Navigation 2.4.0-alpha01 开始,我们开箱即用。
检查 Google Navigation Adavanced Sample
旧答案:
Google samples link
只需将 NavigationExtensions 复制到您的应用程序并通过示例进行配置。效果很好。
经过数小时的研究,我找到了解决方案。它一直就在我们面前 :) 有一个函数:popBackStack(destination, inclusive)
如果在 backStack 中找到,它会导航到给定的目的地。它 returns 布尔值,因此如果控制器找不到片段,我们可以手动导航到那里。
if(findNavController().popBackStack(R.id.settingsFragment, false)) {
Log.d(TAG, "SettingsFragment found in backStack")
} else {
Log.d(TAG, "SettingsFragment not found in backStack, navigate manually")
findNavController().navigate(R.id.settingsFragment)
}
我用过@STAR_ZERO提供的link,效果很好。对于后退按钮有问题的人,您可以像这样在 activity / nav 主机中处理它。
override fun onBackPressed() {
if(navController.currentDestination!!.id!=R.id.homeFragment){
navController.navigate(R.id.homeFragment)
}else{
super.onBackPressed()
}
}
只需检查当前目的地是否是您的根/主页片段(通常是底部导航视图中的第一个片段),如果不是,则导航回该片段,如果是,则仅退出应用程序或执行任何您想做的事情。
顺便说一句,此解决方案需要与 STAR_ZERO 提供的上述解决方案 link 一起使用,使用 keep_state_fragment.
@piotr-prus 提供的解决方案帮助了我,但我不得不添加一些当前目的地检查:
if (navController.currentDestination?.id == resId) {
return //do not navigate
}
如果您错误地导航到当前目的地,则如果不进行此检查,将会重新创建当前目的地,因为它不会在返回堆栈中找到。
如果您在传递参数时遇到问题,请添加:
fragment.arguments = args
在 class KeepStateNavigator
如果您在这里只是为了在使用 BottomNavigationView
和 NavController
的片段之间导航时保持 精确 RecyclerView
滚动状态,那么那里一种简单的方法是将 layoutManager
状态存储在 onDestroyView
并在 onCreateView
我使用 ActivityViewModel 来存储状态。如果您使用不同的方法,请确保将状态存储在父 activity 或任何比片段本身存活时间更长的东西中。
片段
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerview.adapter = MyAdapter()
activityViewModel.listStateParcel?.let { parcelable ->
recyclerview.layoutManager?.onRestoreInstanceState(parcelable)
activityViewModel.listStateParcel = null
}
}
override fun onDestroyView() {
val listState = planet_list?.layoutManager?.onSaveInstanceState()
listState?.let { activityViewModel.saveListState(it) }
super.onDestroyView()
}
视图模型
var plantListStateParcel: Parcelable? = null
fun savePlanetListState(parcel: Parcelable) {
plantListStateParcel = parcel
}
在最新的导航组件版本中 - 底部导航视图将跟踪堆栈中的最新片段。
这是一个示例:
https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample
示例代码
在项目中 build.gradle
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha01"
}
在应用程序中 build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs'
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.4.0-alpha01"
}
在您的 activity 中 - 您可以使用 toolbar
和 bottom navigation view
val navHostFragment = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
//setup with bottom navigation view
binding.bottomNavigationView.setupWithNavController(navController)
//if you want to disable back icon in first page of the bottom navigation view
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.feedFragment,
R.id.favoriteFragment
)
).
//setup with toolbar back navigation
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
现在在您的片段中,您可以导航到第二个片段,当您 deselect/select 底部导航项时 - NavController 会记住堆栈中的最后一个片段。
示例:在您的自定义适配器中
adapter.setOnItemClickListener { item ->
findNavController().navigate(
R.id.action_Fragment1_to_Fragment2
)
}
现在,当您在 fragment 2 中按回键时,NavController 会自动弹出 fragment 1。
https://developer.android.com/guide/navigation/navigation-navigate
自定义通用片段导航的超级简单解决方案:
第 1 步
创建 FragmentNavigator
的子类,根据需要覆盖 instantiateFragment
或 navigate
。如果我们只想创建一次片段,我们可以在这里缓存它,return 在 instantiateFragment
方法中缓存一个。
第 2 步
创建 NavHostFragment
的子类,覆盖 createFragmentNavigator
或 onCreateNavController
,以便可以注入我们自定义的导航器(在步骤 1 中)。
第 3 步
将 android:name="com.example.learn1.navigation.TabNavHostFragment"
中的布局 xml FragmentContainerView
标记属性替换为您自定义的 navHostFragment(在第 2 步中)。