停止使用NavHost在底部导航栏中刷新片段。

25
这个问题已经被问了几次,但现在是2023年了,有人找到好的可用解决方案了吗?
我希望能够使用底部导航控件进行导航,而不必每次选中时刷新 fragment。这是我目前的代码:
navigation/main.xml:
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    app:startDestination="@id/home">

    <fragment
        android:id="@+id/home"
        android:name="com.org.ftech.fragment.HomeFragment"
        android:label="@string/app_name"
        tools:layout="@layout/fragment_home" />
    <fragment
        android:id="@+id/news"
        android:name="com.org.ftech.fragment.NewsFragment"
        android:label="News"
        tools:layout="@layout/fragment_news"/>
    <fragment
        android:id="@+id/markets"
        android:name="com.org.ftech.fragment.MarketsFragment"
        android:label="Markets"
        tools:layout="@layout/fragment_markets"/>
    <fragment
        android:id="@+id/explore"
        android:name="com.org.ftech.ExploreFragment"
        android:label="Explore"
        tools:layout="@layout/fragment_explore"/>
</navigation>

活动邮件模板文件: activity_mail.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout
    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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/main" />

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:itemIconTint="@color/nav"
            app:itemTextColor="@color/nav"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/main">

        </com.google.android.material.bottomnavigation.BottomNavigationView>

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.navigation.NavigationView
        app:menu="@menu/main"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/navigationView"
        android:layout_gravity="start">
    </com.google.android.material.navigation.NavigationView>

</androidx.drawerlayout.widget.DrawerLayout>

主活动(MainActivity).kt:

class MainActivity : AppCompatActivity() {

    private var drawerLayout: DrawerLayout? = null
    private var navigationView: NavigationView? = null
    private var bottomNavigationView: BottomNavigationView? = null
    private lateinit var appBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        drawerLayout = findViewById(R.id.drawer_layout)
        navigationView = findViewById(R.id.navigationView)
        bottomNavigationView = findViewById(R.id.bottomNavigationView)

        val navController = findNavController(R.id.nav_host_fragment)
        appBarConfiguration = AppBarConfiguration(setOf(R.id.markets, R.id.explore, R.id.news, R.id.home), drawerLayout)

        setupActionBarWithNavController(navController, appBarConfiguration)

        findViewById<NavigationView>(R.id.navigationView)
            .setupWithNavController(navController)

        findViewById<BottomNavigationView>(R.id.bottomNavigationView)
            .setupWithNavController(navController)

    }


    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.search) {
            startActivity(Intent(applicationContext, SearchableActivity::class.java))
        }
        return super.onOptionsItemSelected(item)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.options_menu, menu)
        return super.onCreateOptionsMenu(menu)
    }
}

在这个片段中,我会在onCreateView中调用一些服务来获取数据。当恢复片段时,我认为这些调用将不再执行,并且片段的状态应该得到保留。


每个底部导航项都使用导航主机,点击图标时更改它们的可见性。我已经这样做了,而且运行得非常完美。 - Abdul
@Abdul,你能否提供一个示例来演示如何更改可见性,对于那些使用导航宿主的人来说? - baskInEminence
请检查此答案,https://dev59.com/SFIH5IYBdhLWcg3wVcZD#69325054 是一个被接受的答案。 - Rasoul Miri
15个回答

13

试试这个:

public class MainActivity extends AppCompatActivity {


    final Fragment fragment1 = new HomeFragment();
    final Fragment fragment2 = new DashboardFragment();
    final Fragment fragment3 = new NotificationsFragment();
    final FragmentManager fm = getSupportFragmentManager();
    Fragment active = fragment1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);


        BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

        fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
        fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
        fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();

    }


    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
            = new BottomNavigationView.OnNavigationItemSelectedListener() {

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            switch (item.getItemId()) {
                case R.id.navigation_home:
                    fm.beginTransaction().hide(active).show(fragment1).commit();
                    active = fragment1;
                    return true;

                case R.id.navigation_dashboard:
                    fm.beginTransaction().hide(active).show(fragment2).commit();
                    active = fragment2;
                    return true;

                case R.id.navigation_notifications:
                    fm.beginTransaction().hide(active).show(fragment3).commit();
                    active = fragment3;
                    return true;
            }
            return false;
        }
    };


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            startActivity(new Intent(MainActivity.this, SettingsActivity.class));
            return true;
        }

        return super.onOptionsItemSelected(item);
    }


}

或者您可以按照Google的推荐解决方案操作:Google 链接


4
我们许多人都在使用 Nav Host XML 定义片段。对于那个更新的版本,您有没有应用这种解决方案的建议,因为我们不会直接在代码中显式创建片段。 - baskInEminence
@baskInEminence,您有什么要求吗?请向我解释一下。 - Jamil Hasnine Tamim
1
我有一个导航XML文件,其中列出了所有的片段(遵循最新的Android导航模式)。我的主XML活动使用了一个androidx.navigation.fragment.NavHostFragment元素,其键值为(app:navGraph="@navigation/my_nav")。主要活动不创建任何片段,只进行一些小的设置。findViewById<BottomNavigationView>(R.id.bottomNavigationView).setupWithNavController(findNavController(R.id.fragment))。R.id.fragment是我之前提到的NavHostFragment。这种最新的Android模式与您提供的手动创建片段的解决方案不同。 - baskInEminence
1
使用谷歌推荐的答案创建了一个解决方案:https://dev59.com/SFIH5IYBdhLWcg3wVcZD#64142496 - baskInEminence
请问有关于如何升级到多个后退栈的任何想法吗? https://stackoverflow.com/questions/68042591/problem-upgrading-my-fragments-navigation-versionfrom-2-3-5-to-2-4-0-alpha03 - Richard Wilson
显示剩余2条评论

11

防止在同一导航项上多次点击刷新的简单解决方案可能是:

 binding.navView.setOnNavigationItemSelectedListener { item ->
        if(item.itemId != binding.navView.selectedItemId)
            NavigationUI.onNavDestinationSelected(item, navController)
        true
    }

其中binding.navView是使用Android数据绑定引用BottomNavigationView的参考。


1
在这种情况下,您可以简单地使用 binding.navView.setOnNavigationItemReselectedListener { } - Viks
你可以使用 navView.setOnItemReselectedListener { } 替代已弃用的 navView.setOnNavigationItemReselectedListener { } - Vasily Kabunov

11

Kotlin 2020 谷歌推荐使用的解决方案

这些解决方案中,许多在主活动中调用 Fragment 构造函数。然而,按照谷歌推荐的模式,这是不必要的。

设置 Navigation Graph 选项卡

首先,在 res/navigation 目录下为每个选项卡创建一个导航图形 XML 文件。

文件名:tab0.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tab0"
    app:startDestination="@id/fragmentA"
    tools:ignore="UnusedNavigation">

    <fragment
        android:id="@+id/fragmentA"
        android:label="@string/fragment_A_title"
        android:name="com.app.subdomain.fragA"
    >
    </fragment>
</navigation>

请按照上面的模板为您的其他选项卡重复此过程。重要的是,所有片段和导航图都有一个ID(例如@+id/tab0、@+id/fragmentA)。

设置底部导航视图

确保导航ID与底部菜单XML中指定的ID相同。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:title="@string/fragment_A_title"
        android:id="@+id/tab0"
        android:icon="@drawable/ic_baseline_book_24"/>

    <item android:title="@string/fragment_B_title"
        android:id="@+id/tab1"
        android:icon="@drawable/ic_baseline_add_alert_24"/>

    <item android:title="@string/fragment_C_title"
        android:id="@+id/tab2"
        android:icon="@drawable/ic_baseline_book_24"/>

    <item android:title="@string/fragment_D_title"
        android:id="@+id/tab3"
        android:icon="@drawable/ic_baseline_more_horiz_24"/>

</menu>

设置 Activity 主 XML

确保使用的是 FragmentContainerView,而不是 <fragment,并且不要设置 app:navGraph 属性。此属性将在代码中设置。


<androidx.fragment.app.FragmentContainerView
      android:id="@+id/fragmentContainerView"
      android:name="androidx.navigation.fragment.NavHostFragment"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:defaultNavHost="true"
      app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/main_toolbar"
/>

Main Activity XML

将以下代码复制到您的主活动Kotlin文件中,并在OnCreateView中调用setupBottomNavigationBar。确保您的navGraphIds使用R.navigation.whatever而不是R.id.whatever

private lateinit var currentNavController: LiveData<NavController>

private fun setupBottomNavigationBar() {
  val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
  val navGraphIds = listOf(R.navigation.tab0, R.navigation.tab1, R.navigation.tab2, R.navigation.tab3)
  val controller = bottomNavigationView.setupWithNavController(
      navGraphIds = navGraphIds,
      fragmentManager = supportFragmentManager,
      containerId = R.id.fragmentContainerView,
      intent = intent
  )
  controller.observe(this, { navController ->
      val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
      val appBarConfiguration = AppBarConfiguration(navGraphIds.toSet())
      NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
      setSupportActionBar(toolbar)
  })
  currentNavController = controller
}

override fun onSupportNavigateUp(): Boolean {
  return currentNavController?.value?.navigateUp() ?: false
}

复制 NavigationExtensions.kt 文件

将以下文件复制到您的代码库中

[编辑] 上面的链接已经失效。在一个分叉的repo中找到它

来源


1
这是2020年的正确做法,我现在正在我的应用程序中使用这个“navigation”组件。 - faizanjehangir
3
我遵循了你的指南,非常有帮助。不幸的是,导航与之前完全相同,所有片段都被重新创建了。是否有什么建议或者我误解了谷歌的解决方案的意思? - ironflower
你能提供 NavigationExtensions.kt 文件吗?链接已经失效。 - Aan
@AnzyShQ 谢谢您的评论。在一个派生的存储库中找到了它。已更新答案。 - baskInEminence
不要使用这种方法,你需要更新库。请参考此答案:https://dev59.com/SFIH5IYBdhLWcg3wVcZD#69325054 - Rasoul Miri
2
@RasoulMiri 虽然新的 alpha 版本可能会修复这个问题,但它仍然是一个 alpha 发布版本。他们每两周就会发布一个新的 alpha 版本。在部署带有生产代码的 alpha 更改时,请记住这一点。我很高兴看到谷歌已经至少关注了这个问题。 - baskInEminence

6

如果您正在使用Jetpack,最简单的解决方法是使用ViewModel

您必须保存所有有价值的数据,并且不要每次从另一个片段转到一个片段时进行不必要的数据库加载或网络调用。

活动和片段等UI控制器主要用于显示UI数据、响应用户操作或处理操作系统通信,如权限请求。

这就是我们使用ViewModels的时候。

ViewModel对象在配置更改期间自动保留,以便它们所持有的数据即可用于下一个活动或片段实例中。

因此,如果重新创建片段,则所有数据将立即对其可用,而无需再次调用数据库或网络。重要的是要知道,如果持有ViewModel的活动或片段被重新创建,您将收到之前创建的相同的ViewModel实例。

但在这种情况下,您必须指定ViewModel具有活动范围,而不是片段范围,独立于是否为所有片段使用共享ViewModel,还是为每个片段使用不同的ViewModel。

这里是一个小例子,也使用了LiveData:

//Using KTX
val model by activityViewModels<MyViewModel>()
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
        // update UI
    })

//Not using KTX
val model by lazy {ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]}
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
        // update UI
    })

搞定!Google正在积极开发底部标签导航的多个返回堆栈支持,并声称它将在 Navigation 2.4.0 中推出,如此处所说(链接)以及这个问题跟踪器。如果您想了解更多关于多个返回堆栈相关的内容,可以查看这些链接。

请记住,片段仍然会被重新创建,通常您不会改变组件行为,而是将数据适应它们!

我给您留下了一些有用的链接:

ViewModel 概述 Android Developers

如何使用 ViewModels 在片段和活动之间进行通信 - 在 Medium 上

使用 ViewModels 恢复 UI 状态 - 在 Medium 上


如果我们的底部导航视图片段一直显示全屏片段怎么办?假设单个选项卡需要3级深度。那么你不需要一个viewModel来记录用户所在的片段吗?然后在再次创建时快速重新创建这些片段?看起来需要很多工作,并且需要处理导航堆栈,这不是理想的情况。 - baskInEminence
文档中说这是正确的方法,但我并不确定。我尝试了一个简单的整数跟踪,它总是重置回其初始值而不是递增的值。文档本身相当有限,甚至没有提供一个简单的工作示例。 - chitgoks

4

小技巧:如果您只想防止加载已选择的片段,只需覆盖setOnNavigationItemReselectedListener并不执行任何操作即可,但这不会保存片段状态。

binding.navBar.setOnNavigationItemReselectedListener {  }

3

如果您使用的是旧版,建议升级到2.4.0-alpha05 或更高版本。此答案更新于2021年。

androidx.navigation:navigation-runtime-ktx:2.4.0-alpha05
androidx.navigation:navigation-fragment-ktx:2.4.0-alpha05
androidx.navigation:navigation-ui-ktx:2.4.0-alpha05

这个实现只停止了片段刷新吗?似乎没有起作用。你能提供任何代码片段或有效的东西吗? - Aan
这个有效,谢谢。 - Kl3jvi

2
如果您使用NavigationUI.setupWithNavController(),则NavOptions将通过NavigationUI.onNavDestinationSelected()为您定义。这些选项包括launchSingleTop,如果菜单项不是次要的,则还有popUpTo返回到图形的根部。

问题在于,launchSingleTop仍然会用新的片段替换顶部片段。要解决此问题,您需要创建自己的setupWithNavController()onNavDestinationSelected()函数。在onNavDestinationSelected()中,您只需根据需要调整NavOptions即可。


你有一些能够演示这种方法的工作样例吗?我正在查看这里这里,它们能解决这个问题吗? - faizanjehangir
我实际上替换了片段导航器,只显示和隐藏片段。之前我尝试过在这里建议的方法,也可以工作。但那是一个不同的用例。你也可以使用操作来定义行为。 - tynn

1

0

我一直在寻找处理这个问题的最佳方法,最终我想出了这个简单的想法:停用当前选定的菜单项。

这样,您就无法再次单击它,从而防止重新加载片段。

当您通过navHost转到另一个片段时,请不要忘记将其重新启用。

该机制基于NavHostFragment,该片段/活动从中接收您的BottomNavigationView。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    navHostFragment =
        childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment

    val navView: BottomNavigationView = view.findViewById(R.id.nav_view)
    val navController = navHostFragment.navController

    //Here you link the NavHostFragment's navController to your 
    //bottomMenu
    navView.setupWithNavController(navController)

    //Add a listener monitoring the destination changes
    navController.addOnDestinationChangedListener(object : NavController.OnDestinationChangedListener{
        override fun onDestinationChanged(
            controller: NavController,
            destination: NavDestination,
            arguments: Bundle?
        ) {
            /* Disable the selected item and re-enable the others */
            for( item in navView.menu.iterator()){
                item.isEnabled = item.itemId != navView.selectedItemId
            }
        }

    })
}

希望它能有所帮助
Lenzy

0

你好朋友,这是一个新的解决方案:

BottomNavigationView navView = findViewById(R.id.nav_view);

NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);

binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                if (item.getItemId() != binding.navView.getSelectedItemId())
                    NavigationUI.onNavDestinationSelected(item, navController);
                return true;
            }
        });

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