当使用新的NavController和BottomNavigationView时,有没有一种方式可以保持Fragment的活动状态?

142
我正在尝试使用新的导航组件。我使用BottomNavigationView和navController:NavigationUI.setupWithNavController(bottomNavigation, navController)。但是当我切换片段时,它们每次都会被销毁/创建,即使它们之前已经被使用过。有没有办法保持我们的主要片段与BottomNavigationView的链接不断存在?

7
根据 https://issuetracker.google.com/issues/80029773 上的评论,看起来这个问题最终会得到解决。但我还想知道是否有一种便宜的解决方法,在解决问题之前不需要放弃使用该库。 - Jimmy Alexander
11个回答

83

试一下这个。

导航器

创建自定义导航器。

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

导航.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>

活动布局

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

更新

我创建了一个示例项目。链接

我没有创建自定义的NavHostFragment。我使用navController.navigatorProvider += navigator


3
如何保留返回按钮功能?点击返回按钮时应用程序会关闭。 - Francis
6
@jL4,我遇到了同样的问题,可能是你忘记从Activity的NavHostFragment中删除 app:navGraph 属性了。 - Paul Chernenko
10
为什么谷歌没有将这个功能作为默认提供呢? - Arsenius
110
NavigationComponent 应该成为一个解决方案,而不是另一个问题,因此程序员必须创建类似这样的解决方法。 - Arie Agung
4
我的回答已经过时,请查看此存储库 https://github.com/STAR-ZERO/navigation-keep-fragment-sample。 - STAR_ZERO
显示剩余19条评论

37

更新 2021年5月19日 多个后退栈
自Jetpack Navigation 2.4.0-alpha01版本以来,我们已经可以直接使用多个后退栈。 请查看Google Navigation高级示例

旧答案:
Google示例链接 只需将NavigationExtensions复制到您的应用程序中,并按照示例进行配置即可。非常好用。


6
但它仅适用于底部导航。如果您有一般的导航,那么就会遇到问题。 - Georgiy Chebotarev
在复制文件时,请确保为每个 bottomNav 选择创建一个单独的导航图,并确保菜单项的 ID 与图的 ID 匹配。 - Aleyam
请问如何将多个返回栈实现添加到我们的代码中?我一直在尝试,但一直不成功。https://stackoverflow.com/questions/68042591/problem-upgrading-my-fragments-navigation-versionfrom-2-3-5-to-2-4-0-alpha03 - Richard Wilson
我想升级到2.4.0版本,有什么建议吗?我之前使用过旧的示例 - Krahmal
Nav版本2.5.3:唯一的主图起始目标片段仍保留在内存中,其他片段仍然被重新创建。无用的修复,Google在这里也不行。 - Konstantin Konopko
显示剩余4条评论

17

经过多个小时的研究,我找到了解决方法。它一直在我们眼前 :) 这里有一个函数:popBackStack(destination, inclusive),如果在后退栈中找到给定的目标,则导航到该目标。它返回布尔值,所以如果控制器找不到片段,我们可以手动导航到那里。

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

8
如何在哪里使用这个?如果我从Fragment A导航到B,然后从B返回A,如何在不调用onCreateView()和onViewCreated()方法的情况下进行导航,简而言之,如何在不重新启动Fragment A的情况下进行导航。 - Kishan Solanki
1
@UsamaSaeedUS:你能分享一下代码怎么用吗?就像Kishan提到的那样。 - Code_Life

2
如果您在传递参数时遇到问题,请添加以下代码:fragment.arguments = args,并将其放置在KeepStateNavigator类中。

2
如果您只是为了在使用BottomNavigationViewNavController之间导航时保持RecyclerView滚动状态的精确性,那么有一种简单的方法,即在onDestroyView中存储layoutManager状态,并在onCreateView上恢复它。
我使用了ActivityViewModel来存储状态。如果您使用不同的方法,请确保将状态存储在父活动或任何比片段本身更长寿的东西中。
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
}

2
除非在“SavedStateHandle”上实际设置了“viewmodel”中的“Parcelable”,否则这是不正确的。 - EpicPandaForce
@EpicPandaForce 如果您只想在进程仍在运行时保留滚动位置(例如,用户在同一“会话”中切换不同选项卡),则无需使用 SavedStateHandle 也可以实现。 - ubuntudroid
尝试过了,但并不总是有效。 - Jeffrey Liu

1
在最新的导航组件版本中,底部导航视图将跟踪堆栈中的最新片段。
以下是一个示例:

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"

}

在您的活动中,您可以使用工具栏底部导航视图设置导航。
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)

现在,在您的片段中,您可以导航到第二个片段,并且当您取消选择/选择底部导航项时,NavController将记住堆栈中的上一个片段。
例如:在您的自定义适配器中。
adapter.setOnItemClickListener { item ->
            findNavController().navigate(
                R.id.action_Fragment1_to_Fragment2
       )
}

现在,当你在第二个片段内按返回键时,NavController会自动弹出第一个片段。

https://developer.android.com/guide/navigation/navigation-navigate


1
你好。"它将记住堆栈中的上一个片段"--是哪个适配器? - Krahmal
@Krahmal 我写的适配器代码是你的自定义适配器 - 你可以导航到第二个片段。后退堆栈由Android导航组件NavController类自动管理。https://developer.android.com/guide/navigation/navigation-navigate#back-stack - rafsanahmad007
1
你的答案是如何在按下返回键时转到最近访问的Fragment,但不会恢复Fragment的先前工作。问题与恢复Fragment的先前工作有关。 - 4rigener

1

目前不可用。

作为一种解决方法,您可以将所有获取的数据存储到ViewModel中,并在重新创建片段时立即使用该数据。确保使用活动上下文获取ViewModel对象。

您可以使用LiveData使您的数据具有生命周期感知能力并成为可观察的数据持有者。


3
确实如此,数据>视图,但问题在于当您希望保留视图状态时,例如已加载多个页面且用户已向下滚动时。 - Joaquim Ley
@JoaquimLey 视图状态从未被设计为在用户导航离开活动或片段时进行存储,就像在这种情况下一样。然而,在系统限制导致进程死亡的情况下,将其存储在“savedInstanceState”包中可能会很有用。 - Samuel Robert

1

自定义通用片段导航的超级简单解决方案:

步骤1

创建FragmentNavigator的子类,根据需要重写instantiateFragmentnavigate方法。如果我们希望片段只创建一次,可以在这里缓存它,并在instantiateFragment方法中返回缓存的片段。

步骤2

创建NavHostFragment的子类,覆盖createFragmentNavigatoronCreateNavController,以便注入我们的自定义导航器(在步骤1中)。

步骤3

将布局xml中的FragmentContainerView标记属性从android:name="com.example.learn1.navigation.TabNavHostFragment"替换为您自定义的navHostFragment(在步骤2中)。


1
我已使用@STAR_ZERO提供的链接,它运行良好。对于那些在使用后退按钮时遇到问题的人,您可以在活动/导航宿主中像这样处理它。
override fun onBackPressed() {
        if(navController.currentDestination!!.id!=R.id.homeFragment){
            navController.navigate(R.id.homeFragment)
        }else{
            super.onBackPressed()
        }
    }

只需检查当前目标是否为根/主页片段(通常在底部导航视图中的第一个),如果不是,只需导航回该片段,如果是,则仅退出应用程序或执行您想要的任何操作。

顺便说一下,此解决方案需要与上面由STAR_ZERO提供的解决方案一起使用,使用keep_state_fragment。


1
我不知道为什么,但currentDestination!!.id在所有3个片段中始终给出相同的值。 - Avishek Das

0

@piotr-prus 提供的解决方案对我很有帮助,但我还需要添加一些当前目标检查:

if (navController.currentDestination?.id == resId) {
    return       //do not navigate
}

如果没有这个检查,当前目标页面如果被误导航到,将会重新创建,因为它不在返回堆栈中。


很高兴我的解决方案对您有用。实际上,在从backStack调用fragment之前,我会检查bottomNavigationView中的current menuItem,使用:bottomNavigationView.setOnNavigationItemSelectedListener。 - Piotr Prus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接