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」。
相关代码
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/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 } }
验证效果
修改后按照异常流程测试:
- 从
BreakingNewsFragment跳转到ArticleFragment; - 点击「Saved News」切换到
SavedNewsFragment; - 再次点击「Breaking News」,会正确回到
BreakingNewsFragment,底部导航激活标签同步切换。
内容的提问来源于stack exchange,提问作者PopJoestar
相关产品推荐
相关产品推荐

