You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kotlin BottomNavigationView导航异常问题求助

问题描述

我开发了一个单Activity架构的Android应用,通过BottomNavigationView展示3个Fragment:BreakingNewsFragment、SavedNewsFragment和SearchNewsFragment,这三个Fragment均展示网络获取的文章列表。点击任意Fragment中的文章,可导航至ArticleFragment,通过WebView展示文章详情。

操作异常流程:

  • 从BreakingNewsFragment跳转到ArticleFragment时,底部导航的「Breaking News」标签处于激活状态;
  • 点击「Saved News」标签切换到SavedNewsFragment,激活标签变为「Saved News」;
  • 再次点击「Breaking News」标签,本该回到BreakingNewsFragment,却显示了ArticleFragment,且底部导航激活标签仍为「Saved News」。

相关代码

<?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/news_nav_graph"
    app:startDestination="@id/breakingNewsFragment">

    <fragment
        android:id="@+id/articleFragment"
        android:name="com.androiddevs.mvvmnewsapp.ui.fragments.ArticleFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right">
        <argument
            android:name="url"
            app:argType="string" />
    </fragment>
    <fragment
        android:id="@+id/breakingNewsFragment"
        android:name="com.androiddevs.mvvmnewsapp.ui.fragments.BreakingNewsFragment"
        android:label="Breaking News" >
        <action
            android:id="@+id/action_breakingNewsFragment_to_articleFragment"
            app:destination="@id/articleFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/savedNewsFragment"
        android:name="com.androiddevs.mvvmnewsapp.ui.fragments.SavedNewsFragment"
        android:label="Saved" >
        <action
            android:id="@+id/action_savedNewsFragment_to_articleFragment"
            app:destination="@id/articleFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/searchNewsFragment"
        android:name="com.androiddevs.mvvmnewsapp.ui.fragments.SearchNewsFragment"
        android:label="SearchNewsFragment"
        >
        <action
            android:id="@+id/action_searchNewsFragment_to_articleFragment"
            app:destination="@id/articleFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
</navigation>

主Activity代码

package com.androiddevs.mvvmnewsapp.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.androiddevs.mvvmnewsapp.R
import com.androiddevs.mvvmnewsapp.db.ArticleDatabase
import com.androiddevs.mvvmnewsapp.repository.NewsRepository
import kotlinx.android.synthetic.main.activity_news.*

class NewsActivity : AppCompatActivity() {

    private lateinit var navController: NavController
    lateinit var viewModel: NewsViewModel

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

        val navHostFrag = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
        navController   = navHostFrag.navController

        val newsRepository = NewsRepository(ArticleDatabase(this))
        val newsViewModelProviderFactory = NewsViewModelProviderFactory(newsRepository)
        viewModel = ViewModelProvider(this, newsViewModelProviderFactory).get(NewsViewModel::class.java)

        bottomNavigationView.setupWithNavController(navController)
    }
}

Activity布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.NewsActivity">

    <FrameLayout
        android:id="@+id/flFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/newsNavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:defaultNavHost="true"
            app:navGraph="@navigation/news_nav_graph"
            />

    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="match_parent"
        app:menu="@menu/bottom_nav_menu"
        android:layout_height="56dp"
        app:backgroundTint="@color/design_default_color_surface"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
解决方案

问题核心是Navigation组件的返回栈管理逻辑:从BreakingNewsFragment跳转到ArticleFragment后,返回栈保留了这两个页面实例;切换到SavedNewsFragment时返回栈追加新页面;再次点击「Breaking News」时,Navigation默认回到该目的地的最后一个实例(即ArticleFragment),而非主Fragment。

推荐两种解决方法:

方案1:使用嵌套导航图(规范方案)

将每个底部导航对应的Fragment设置为独立嵌套导航图,让每个标签拥有自己的返回栈,切换时自动回到对应主Fragment。

修改nav_graph.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"
    android:id="@+id/news_nav_graph"
    app:startDestination="@id/breakingNewsGraph">

    <!-- 公共的ArticleFragment -->
    <fragment
        android:id="@+id/articleFragment"
        android:name="com.androiddevs.mvvmnewsapp.ui.fragments.ArticleFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right">
        <argument
            android:name="url"
            app:argType="string" />
    </fragment>

    <!-- Breaking News 嵌套图 -->
    <navigation
        android:id="@+id/breakingNewsGraph"
        android:label="Breaking News"
        app:startDestination="@id/breakingNewsFragment">
        <fragment
            android:id="@+id/breakingNewsFragment"
            android:name="com.androiddevs.mvvmnewsapp.ui.fragments.BreakingNewsFragment"
            android:label="Breaking News" >
            <action
                android:id="@+id/action_breakingNewsFragment_to_articleFragment"
                app:destination="@id/articleFragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"/>
        </fragment>
    </navigation>

    <!-- Saved News 嵌套图 -->
    <navigation
        android:id="@+id/savedNewsGraph"
        android:label="Saved"
        app:startDestination="@id/savedNewsFragment">
        <fragment
            android:id="@+id/savedNewsFragment"
            android:name="com.androiddevs.mvvmnewsapp.ui.fragments.SavedNewsFragment"
            android:label="Saved" >
            <action
                android:id="@+id/action_savedNewsFragment_to_articleFragment"
                app:destination="@id/articleFragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"/>
        </fragment>
    </navigation>

    <!-- Search News 嵌套图 -->
    <navigation
        android:id="@+id/searchNewsGraph"
        android:label="Search"
        app:startDestination="@id/searchNewsFragment">
        <fragment
            android:id="@+id/searchNewsFragment"
            android:name="com.androiddevs.mvvmnewsapp.ui.fragments.SearchNewsFragment"
            android:label="Search" >
            <action
                android:id="@+id/action_searchNewsFragment_to_articleFragment"
                app:destination="@id/articleFragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"/>
        </fragment>
    </navigation>
</navigation>

修改底部导航菜单(bottom_nav_menu.xml),让菜单项ID对应嵌套图ID:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/breakingNewsGraph"
        android:icon="@drawable/ic_breaking_news"
        android:title="Breaking News"/>
    <item
        android:id="@+id/savedNewsGraph"
        android:icon="@drawable/ic_saved"
        android:title="Saved"/>
    <item
        android:id="@+id/searchNewsGraph"
        android:icon="@drawable/ic_search"
        android:title="Search"/>
</menu>

方案2:自定义底部导航点击事件

若不想用嵌套图,可自定义点击监听器,强制导航到主Fragment并清空对应返回栈:

修改主Activity代码:

package com.androiddevs.mvvmnewsapp.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import com.androiddevs.mvvmnewsapp.R
import com.androiddevs.mvvmnewsapp.db.ArticleDatabase
import com.androiddevs.mvvmnewsapp.repository.NewsRepository
import kotlinx.android.synthetic.main.activity_news.*

class NewsActivity : AppCompatActivity() {

    private lateinit var navController: NavController
    lateinit var viewModel: NewsViewModel

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

        val navHostFrag = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
        navController   = navHostFrag.navController

        val newsRepository = NewsRepository(ArticleDatabase(this))
        val newsViewModelProviderFactory = NewsViewModelProviderFactory(newsRepository)
        viewModel = ViewModelProvider(this, newsViewModelProviderFactory).get(NewsViewModel::class.java)

        // 自定义底部导航点击逻辑
        bottomNavigationView.setOnItemSelectedListener { menuItem ->
            // 清空目标目的地之上的所有返回栈
            val navOptions = androidx.navigation.NavOptions.Builder()
                .setPopUpTo(menuItem.itemId, true)
                .build()
            NavigationUI.onNavDestinationSelected(menuItem, navController)
            navController.navigate(menuItem.itemId, null, navOptions)
            true
        }

        // 初始化选中状态
        bottomNavigationView.selectedItemId = R.id.breakingNewsFragment
    }
}
验证效果

修改后按照异常流程测试:

  1. 从BreakingNewsFragment跳转到ArticleFragment;
  2. 点击「Saved News」切换到SavedNewsFragment;
  3. 再次点击「Breaking News」,会正确回到BreakingNewsFragment,底部导航激活标签同步切换。

内容的提问来源于stack exchange,提问作者PopJoestar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 17:18:23