【问题标题】:Android Navigation Component back button not workingAndroid导航组件后退按钮不起作用
【发布时间】: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_width="match_parent"
        android:layout_height="match_parent">

    <fragment
            android:id="@+id/dashboard_navigation"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            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; 标记)?后退按钮是否适用于您拥有的所有其他目的地?

标签: android android-fragments android-navigation


【解决方案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_width="match_parent"
        android:layout_height="match_parent"
        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()
              }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-12-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多