Android导航组件后退按钮不起作用
Posted
技术标签:
【中文标题】Android导航组件后退按钮不起作用【英文标题】:Android Navigation Component back button not working 【发布时间】:2020-05-07 02:50:38 【问题描述】:我在 android 中使用导航组件,我最初设置了 6 个片段。问题是当我添加一个新片段(ProfileFragment)时。
当我从起始目的地导航到这个新片段时,按下本机后退按钮不会弹出当前片段。相反,它只是停留在我所在的片段中 - 后退按钮什么都不做。
这是我的 navigation.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/dashboard_navigation"
app:startDestination="@id/dashboardFragment"
>
<fragment
android:id="@+id/dashboardFragment"
android:name="com.devssocial.localodge.ui.dashboard.ui.DashboardFragment"
android:label="DashboardFragment"
>
<action
android:id="@+id/action_dashboardFragment_to_newPostFragment"
app:destination="@id/newPostFragment"
app:enterAnim="@anim/slide_in_up"
app:exitAnim="@anim/slide_out_down"
app:popEnterAnim="@anim/slide_in_up"
app:popExitAnim="@anim/slide_out_down"
/>
<action
android:id="@+id/action_dashboardFragment_to_notificationsFragment"
app:destination="@id/notificationsFragment"
app:enterAnim="@anim/slide_in_up"
app:exitAnim="@anim/slide_out_down"
app:popEnterAnim="@anim/slide_in_up"
app:popExitAnim="@anim/slide_out_down"
/>
<action
android:id="@+id/action_dashboardFragment_to_mediaViewer"
app:destination="@id/mediaViewer"
app:enterAnim="@anim/slide_in_up"
app:exitAnim="@anim/slide_out_down"
app:popEnterAnim="@anim/slide_in_up"
app:popExitAnim="@anim/slide_out_down"
/>
<action
android:id="@+id/action_dashboardFragment_to_postDetailFragment"
app:destination="@id/postDetailFragment"
app:enterAnim="@anim/slide_in_up"
app:exitAnim="@anim/slide_out_down"
app:popEnterAnim="@anim/slide_in_up"
app:popExitAnim="@anim/slide_out_down"
/>
====================== HERE'S THE PROFILE ACTION ====================
<action
android:id="@+id/action_dashboardFragment_to_profileFragment"
app:destination="@id/profileFragment"
app:enterAnim="@anim/slide_in_up"
app:exitAnim="@anim/slide_out_down"
app:popEnterAnim="@anim/slide_in_up"
app:popExitAnim="@anim/slide_out_down"
/>
=====================================================================
</fragment>
<fragment
android:id="@+id/profileFragment"
android:name="com.devssocial.localodge.ui.profile.ui.ProfileFragment"
android:label="fragment_profile"
tools:layout="@layout/fragment_profile"
/>
</navigation>
在上图中,突出显示的箭头(左侧)是我遇到问题的导航操作。
在我的 Fragment 代码中,我的导航如下:
findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)
其他导航操作按预期工作。但是由于某种原因,这个新添加的片段并没有按预期运行。
当我导航到 ProfileFragment 和按下后退按钮时,没有显示任何日志。
我错过了什么吗?还是我的动作/片段配置有什么问题?
编辑: 我没有在 ProfileFragment 中做任何事情。这是它的代码:
class ProfileFragment : Fragment()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_profile, container, false)
还有我的 activity xml 包含导航主机:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_>
<fragment
android:id="@+id/dashboard_navigation"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_
android:layout_
app:navGraph="@navigation/dashboard_navigation"
app:defaultNavHost="true"/>
</FrameLayout>
【问题讨论】:
this.findNavController().popBackStack()
会为你工作
您的ProfileFragment
是否实现了任何custom back navigation?您在那里执行的任何自定义后退代码都将优先于 NavController。你能包含那个片段的代码吗?
@ianhanniballake 我没有在 ProfileFragment 中做任何事情。我已经编辑了我的问题以包含它
@RakeshKumar 对不起,我不完全理解你的意思。我在哪里添加 this.findNavController().popBackStack()
?并且我的仪表板操作已经有目的地作为 profileFragment。
您能否包括将NavHostFragment
添加到您的活动的位置(我假设通过您的布局XML 中的<fragment>
标记)?后退按钮是否适用于您拥有的所有其他目的地?
【参考方案1】:
如果您在导航组件中使用 setupActionBarWithNavController 例如:
setupActionBarWithNavController(findNavController(R.id.fragment))
然后在您的主要活动中覆盖并配置此方法:
override fun onSupportNavigateUp(): Boolean
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
我的 MainActivity.kt
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(findNavController(R.id.fragment))
override fun onSupportNavigateUp(): Boolean
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
【讨论】:
完美。谢谢! 效果很好,谢谢【参考方案2】:对于在作为 Home Fragment 的前一个 Fragment 中使用 LiveData 的任何人,每当您通过按返回按钮返回到前一个 Fragment 时,Fragment 就会开始观察数据,并且因为 ViewModel 在此操作中幸存下来,它会立即发出最后一个发出的值在我的情况下,它打开了我按下后退按钮的片段,这样看起来后退按钮不起作用,解决方案是使用只发出一次数据的东西。我用过这个:
class SingleLiveData<T> : MutableLiveData<T>()
private val pending = AtomicBoolean()
/**
* Adds the given observer to the observers list within the lifespan of the given
* owner. The events are dispatched on the main thread. If LiveData already has data
* set, it will be delivered to the observer.
*
* @param owner The LifecycleOwner which controls the observer
* @param observer The observer that will receive the events
* @see MutableLiveData.observe
*/
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>)
super.observe(owner, Observer t ->
if (pending.compareAndSet(true, false))
observer.onChanged(t)
)
/**
* Sets the value. If there are active observers, the value will be dispatched to them.
*
* @param value The new value
* @see MutableLiveData.setValue
*/
@MainThread
override fun setValue(value: T?)
pending.set(true)
super.setValue(value)
【讨论】:
非常适合我。谢谢为我节省了很多时间。 哦,伙计们,非常感谢,我已经为此卡了 2 个小时! 哦,我花了两个小时试图找出问题所在,谢谢 我使用了 singleEventLivedata 而不是 mutableliveData 问题解决了 LiveData 和 Navigation 的作者多次说过,SingleLiveEvent 实际上是一种反模式,因为它滥用了 LiveData reddit.com/r/androiddev/comments/dz1q3f/…【参考方案3】:您可以将以下内容用于Activity
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true)
override fun handleOnBackPressed()
onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
)
对于fragment
,需要requireActivity()
以及回调
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true)
override fun handleOnBackPressed()
requireActivity().onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
)
如果您有 Button
或其他东西来执行操作,那么您可以使用
this.findNavController().popBackStack()
【讨论】:
您绝对不应该从OnBackPressedCallback
调用onBackPressed()
- 这肯定会导致无限循环。 NavController
设置自己的OnBackPressedCallback
反正你不需要这样做。
这将是必需的。在onBackPressed()
上执行某项任务
@RakeshKumar 感谢您的回答!我试过你的方法,但不幸的是它不起作用。导航到其他片段会导致应用崩溃。
从片段中调用requireActivity().onBackPressed()
会导致infinite loop
并且应用程序崩溃。
添加到@ianhanniballake 评论,如果你曾经遇到过一个你想忽略 onBackPress 的用例(即在后按时什么都不做),你可以通过添加 this.remove()
来删除被调用后的回调并通过将相同的 callBack 作为高阶函数传递给执行正常后退行为 requireActivity().onBackPressed()
的父函数再次添加它,可以在子片段中覆盖【参考方案4】:
这个问题发生在我使用MutableLiveData
在片段之间导航并且观察多个片段的实时数据对象时。
我通过仅观察一次实时数据对象或使用SingleLiveEvent 而不是MutableLiveData
解决了这个问题。因此,如果您在这里遇到相同的情况,请尝试仅观察一次实时数据对象或使用SingleLiveEvent
。
【讨论】:
【参考方案5】:导航完成后,您需要将 MutableLiveData 设置为 null。
例如
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
fun printName()
_name.value = "John"
fun navigationComplete()
_name.value = null
现在假设您正在观察片段中的“名称”,并且一旦名称为 John,您正在进行一些导航,那么应该是这样的:
viewModel.name.observe(viewLifecycleOwner, Observer name ->
when (name)
"John" ->
this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
viewModel.navigationComplete()
)
现在您的后退按钮可以正常工作了。
有些数据几乎只使用一次,例如 Snackbar 消息或导航事件,因此您必须告诉在使用完毕后将值设置为 null。
问题是 _name 中的值仍然为真,无法返回到上一个片段。
【讨论】:
非常适合我。谢谢为我节省了很多时间。【参考方案6】:如果您使用 Moxy 或类似的库,请在从一个片段导航到第二个片段时检查该策略。
当策略为AddToEndSingleStrategy
时,我遇到了同样的问题。
您需要将其更改为SkipStrategy
。
interface ZonesListView : MvpView
@StateStrategyType(SkipStrategy::class)
fun navigateToChannelsList(zoneId: String, zoneName: String)
【讨论】:
感谢您的回答!但是我没有使用任何库,但我会记住这一点。【参考方案7】:在 OnCreateView 中调用 onBackPressed
private fun onBackPressed()
requireActivity().onBackPressedDispatcher.addCallback(this)
//Do something
【讨论】:
【参考方案8】:对于使用 LiveData 设置导航 ID 的每个人,都无需使用 SingleLiveEvent。设置初始值后,您可以将destinationId设置为null。
例如,如果您想从片段 A 导航到 B。
视图模型 A:
val destinationId = MutableLiveData<Int>()
fun onNavigateToFragmentB()
destinationId.value = R.id.fragmentB
destinationId.value = null
这仍然会触发 Fragment 中的 Observer 并进行导航。
片段 A
viewModel.destinationId.observe(viewLifecycleOwner, destinationId ->
when (destinationId)
R.id.fragmentB -> navigateTo(destinationId)
)
【讨论】:
【参考方案9】:您的问题的最简单答案(如果它与片段有关 - 底部导航)可能是
设置 defaultNavHost = "false"
从官方文档中说-> 假设您为底部导航设置了 3 个片段,然后设置
"defaultNavHost = true" 将使片段 A 像父级一样,因此当用户单击片段 3 中的后退按钮时,它会到达片段 1 而不是关闭活动(以底部导航为例)。
如果您只想按下并关闭您所在的任何片段中的活动,您的 XML 应该如下所示。
<fragment
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_
android:layout_
android:layout_above="@+id/bottom_nav"
app:defaultNavHost="false"
app:navGraph="@navigation/visit_summary_navigation" />
【讨论】:
【参考方案10】:导航后将 MutableLiveData 设置为 false
将此代码放入您的 ViewModel.kt
private val _eventNextFragment = MutableLiveData<Boolean>()
val eventNextFragment: LiveData<Boolean>
get() = _eventNextFragment
fun onNextFragment()
_eventNextFragment.value = true
fun onNextFragmentComplete()
_eventNextFragment.value = false
假设您要导航到另一个片段,您将在导航操作后立即从 viewModel 调用 onNextFragmentComplete 方法。
将此代码放入您的 Fragment.kt
private fun nextFragment()
val action = actionFirstFragmentToSecondFragment()
NavHostFragment.findNavController(this).navigate(action)
viewModel.onNextFragmentComplete()
【讨论】:
以上是关于Android导航组件后退按钮不起作用的主要内容,如果未能解决你的问题,请参考以下文章