使用 navhost 在底部导航中停止片段刷新
Posted
技术标签:
【中文标题】使用 navhost 在底部导航中停止片段刷新【英文标题】:Stop fragment refresh in bottom nav using navhost 【发布时间】:2020-05-21 00:18:36 【问题描述】:这个问题已经被问过好几次了,但我们现在是 2020 年,有没有人找到一个很好的可用解决方案?
我希望能够使用底部导航控件进行导航,而无需在每次选择片段时都刷新片段。这是我目前拥有的:
导航/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_
android:layout_
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_
android:layout_>
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_
android:layout_
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_
android:layout_
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_
android:layout_
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 你能提供一个示例来改变我们这些使用导航主机的人的可见性吗? 检查这个答案,***.com/a/69325054/4797289 是一个可接受的答案。 【参考方案1】:试试这个:
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 Link
【讨论】:
我们中的许多人都在使用 nav Host xml 来定义我们的片段。关于将此解决方案应用于较新版本的任何建议,因为我们不会直接在代码中的活动中显式创建片段 @baskInEminence 你有什么要求?请解释一下。 我有一个导航 xml,其中列出了我的所有片段(遵循最新的 android 导航模式)。我的活动主 xml 使用带有键值 (app:navGraph="@navigation/my_nav") 的 androidx.navigation.fragment.NavHostFragment 元素。主要活动不创建任何片段,仅次要设置。 findViewById在多次点击同一导航项时停止刷新的简单解决方案可能是
binding.navView.setOnNavigationItemSelectedListener item ->
if(item.itemId != binding.navView.selectedItemId)
NavigationUI.onNavDestinationSelected(item, navController)
true
其中 binding.navView 是使用 Android 数据绑定的 BottomNavigationView 的参考。
【讨论】:
在这种情况下你可以简单地做binding.navView.setOnNavigationItemReselectedListener
您可以使用navView.setOnItemReselectedListener
代替已弃用的navView.setOnNavigationItemReselectedListener
【参考方案3】:
Kotlin 2020 谷歌推荐解决方案
其中许多解决方案在 Main Activity 中调用 Fragment 构造函数。但是,按照 Google 推荐的模式,这不是必需的。
设置导航图标签
首先为res/navigation
目录下的每个选项卡创建一个navigation graph 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 中指定的相同。
<?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>
设置活动主 XML
确保使用的是 FragmentContainerView
而不是 <fragment
,并且不要设置 app:navGraph 属性。这将在稍后的代码中设置
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_
android:layout_
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"
/>
主要活动 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 文件
将以下file 复制到您的代码库
[编辑] 上面的链接已损坏。在一个分叉的repo 中找到它
来源
谷歌的Solution【讨论】:
这是 2020 年的正确方式,我现在在我的应用程序中使用这个navigation
组件。
我已按照您的指南进行操作,非常有帮助。不幸的是,导航的工作方式与以前完全相同,正在重新创建所有片段。任何建议或我是否误解了谷歌解决方案应该做什么。
您能否提供此 NavigationExtensions.kt 文件,给定链接已损坏
@AnzyShQ 感谢您的评论。在一个分叉的仓库中找到它。更新答案
@RasoulMiri 虽然新的 alpha 版本可能会修复它,但它仍然是 alpha 版本。他们每 2 周不断发布此版本的新 Alpha。在使用生产代码部署 alpha 更改时请记住这一点。我很高兴看到 Google 至少已经解决了这个问题【参考方案4】:
如果您使用的是 Jetpack,解决此问题的最简单方法是使用 ViewModel
您必须保存所有有价值的数据,并且不要在每次从另一个片段转到片段时进行不必要的数据库加载或网络调用。
UI controllers such as activities and fragments are primarily intended to display UI data, react to user actions, or handle operating system communication, such as permission requests.
这是我们使用ViewModels
的时候
ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.
因此,如果重新创建片段,您的所有数据将立即存在,而不是再次调用数据库或网络。重要的是要知道,如果重新创建包含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
)
就是这样!谷歌正在积极致力于底部标签导航的多个后退堆栈支持,并声称它将到达Navigation 2.4.0
,如here 和on this issue tracker 所说,如果您愿意和/或您的问题与多个后退堆栈更相关,您可以查看这些链接
请记住,仍然会重新创建片段,通常您不会更改组件的行为,而是让数据适应它们!
我给你留下一些有用的链接:
ViewModel Overview Android Developers
How to communicate between fragments and activities with ViewModels - on Medium
Restoring UI State using ViewModels - on Medium
【讨论】:
如果我们的底部导航视图片段一直显示全屏片段怎么办?可以说该单个选项卡的深度为 3 级。那么你不需要一个视图模型来记录用户在哪个片段上吗?然后在再次创建这些片段时快速重新创建这些片段?看起来工作量很大,需要处理不太理想的导航堆栈 文档说这是要走的路。虽然我不确定是不是。我尝试了一个简单的 int 跟踪,它总是重置回其初始值而不是增加的值。文档本身相当有限,甚至没有提供简单的工作示例。【参考方案5】:快速提示,如果您只想阻止加载已选择的片段,只需覆盖 setOnNavigationItemReselectedListener
并且什么都不做,但这不会保存片段状态
binding.navBar.setOnNavigationItemReselectedListener
【讨论】:
【参考方案6】:如果您使用NavigationUI.setupWithNavController()
,则NavOptions
将使用NavigationUI.onNavDestinationSelected()
为您定义。这些选项包括launchSingleTop
,如果菜单项不是二级菜单项,则还包括popUpTo
图的根。
问题是,launchSingleTop
仍然用新的片段替换顶部片段。要解决此问题,您必须创建自己的 setupWithNavController()
和 onNavDestinationSelected()
函数。在onNavDestinationSelected()
中,您只需根据您的需要调整NavOptions
。
【讨论】:
你有一些工作样本来演示这种方法吗?我在看here和here,这些能解决这个问题吗? 我实际上替换了片段导航器,只显示和隐藏片段。在我尝试做我在这里建议的事情之前,它也很有效。但这是一个不同的用例。您还可以使用操作来定义行为。【参考方案7】:您使用的是旧版本,您只需使用 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
【讨论】:
这个实现是否只停止片段刷新?似乎不起作用。你能提供任何 sn-p 或其他工作吗? 这行得通,谢谢【参考方案8】:试试这样的
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
和
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener item ->
when (item.itemId)
R.id.home ->
fragmentManager.beginTransaction().hide(active).show(homeFragment).commit()
active = homeFragment
return@OnNavigationItemSelectedListener true
R.id.news ->
fragmentManager.beginTransaction().hide(active).show(newsFragment).commit()
active = newsFragment
return@OnNavigationItemSelectedListener true
R.id.markets ->
fragmentManager.beginTransaction().hide(active).show(marketsFragment).commit()
active = marketsFragment
return@OnNavigationItemSelectedListener true
R.id.explore ->
fragmentManager.beginTransaction().hide(active).show(exploreFragment).commit()
active = exploreFragment
return@OnNavigationItemSelectedListener true
false
【讨论】:
我是否需要在supportFragmentManager
中设置 fragments
才能尝试在侦听器中导航它们?
是的。将它们全部设置在 Activity 的 OnCreate(...) 中。确保默认为活动,其余为隐藏
OnViewCreated 将以任何一种方式执行。它总是被称为 distegarding 您如何使用 fragmentManager - 经典方式或通过喷气背包导航。
确保您首先通过FindFragmentByTag
检查片段是否存在来获取对片段的引用,并使用onSaveInstanceState / onCreate(Bundle)跟踪活动片段标签【参考方案9】:
创建一个类:
@Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml
class KeepStateNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
) : FragmentNavigator(context, manager, containerId)
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination?
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
var initialNavigate = false
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null)
transaction.detach(currentFragment)
else
initialNavigate = true
var fragment = manager.findFragmentByTag(tag)
if (fragment == null)
val className = destination.className
fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
transaction.add(containerId, fragment, tag)
else
transaction.attach(fragment)
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commitNow()
return if (initialNavigate)
destination
else
null
在 nav_graph 中使用 keep_state_fragment 而不是片段
活动中:
val navController = findNavController(R.id.nav_host_fragment)
// get fragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
// setup custom navigator
val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
navController.navigatorProvider += navigator
// set navigation graph
navController.setGraph(R.navigation.nav_graph)
bottom_navigation.setupWithNavController(navController)
【讨论】:
这不起作用。该片段始终为空,因此即使我在 navGraph 中使用了使用这个sn-p:
private fun attachFragment(fragmentTag: String)
val fragmentTransaction = supportFragmentManager.beginTransaction()
supportFragmentManager.findFragmentByTag(fragmentTag)?.let
if (supportFragmentManager.backStackEntryCount == 0) return
val currentFragmentTag = supportFragmentManager.getBackStackEntryAt(supportFragmentManager.backStackEntryCount - 1).name
(supportFragmentManager.findFragmentByTag(currentFragmentTag) as? FragmentBase)?.let curFrag ->
fragmentTransaction.hide(curFrag)
fragmentTransaction.show(it)
?: run
when (fragmentTag)
FragmentHome.TAG -> FragmentBase.newInstance<FragmentHome>()
FragmentAccount.TAG -> FragmentBase.newInstance<FragmentAccount>()
else -> null
?.let
fragmentTransaction.add(R.id.container, it, fragmentTag)
fragmentTransaction.addToBackStack(fragmentTag)
fragmentTransaction.commit()
您可以使用此方法传递您现在要显示的特定片段的标签,使用方法attachFragment(FragmentHome.TAG)
【讨论】:
【参考方案11】:如果你使用的是导航组件,除了this answer From version:'2.4.0-alpha01'它还内置了对多个返回栈的支持。所以不需要导航扩展
有关详细信息,请参阅此链接。 https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
【讨论】:
【参考方案12】:你好朋友,这是新的解决方案:
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;
);
【讨论】:
【参考方案13】:我一直在寻找处理这个问题的最佳方法,最后我想出了一个简单的想法:停用当前选择的 MenuItem。
这样,您不能在其上单击两次,因此会阻止重新加载片段。
当您通过 navHost 转到另一个片段时,不要忘记启用它。
该机制基于从片段/活动中接收您的 BottomNavigationView 的 NavHostFragment。
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
)
希望对你有帮助
伦齐
【讨论】:
以上是关于使用 navhost 在底部导航中停止片段刷新的主要内容,如果未能解决你的问题,请参考以下文章
如何用NavHost+底部导航栏完成fragmetn切换效果