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 中的&lt;fragment&gt; 标记)?后退按钮是否适用于您拥有的所有其他目的地? 【参考方案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导航组件后退按钮不起作用的主要内容,如果未能解决你的问题,请参考以下文章

自定义后退按钮图像在导航栏上不起作用

导航栏上的自定义后退按钮不起作用

导航到上一个视图控制器,默认后退按钮不起作用

设置 uinavigationitem 的后退按钮不起作用

ANDROID:简单的向上导航不起作用

NavigationBar 后退按钮奇怪的行为并且不起作用