安卓导航组件返回按钮无法正常工作

39
我在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>

在此输入图像描述

上图中,突出显示的箭头(左侧)是我遇到问题的导航操作。

在我的片段代码中,我按以下方式进行导航:

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)
    }


}

我的活动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>

1
this.findNavController().popBackStack() 会对您有所帮助。 - ॐ Rakesh Kumar
你能否在添加 NavHostFragment 到你的 Activity 时包含它的位置(我假设是通过在你的布局 XML 中使用 <fragment> 标签)?你所有其他目标页面上的返回按钮都能正常工作吗? - ianhanniballake
@ChristilynArjona 你解决了这个问题吗?我也遇到了同样的问题... 这里的原因是什么? - Bulma
有没有解决方案?我有一个几乎相似的配置,使用了4个选项卡的BottonNavigationView,我使用最新的支持多个BackStacks的androidx-fragment alpha。所有其他选项卡都可以正确地在它们自己的backstacks中导航,但其中有一个选项卡根本不会返回! popBackstacknavigateUp和按返回按钮都无效。我应该发布一个新的SO问题来描述我的设置吗? - Kshitij Patil
@KshitijPatil 是的,最好发布一个新的帖子。这个有点过时了,而且我也没有跟踪与此代码相关的项目。你可以尝试一种巧妙的方法,即尝试深度链接到新片段。 - Christilyn Arjona
显示剩余7条评论
11个回答

65

如果您正在使用导航组件中的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()
}
}

it's perfect. Thank you! - Hoang Nguyen
完美运行,谢谢。 - Vibin

45

如果您在之前的一个主页片段中使用LiveData,每当您按下返回按钮回到之前的片段时,该片段会开始观察数据,并且由于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)
}

1
哦,太感谢了!我被这个问题卡了两个小时了! - Armando Ávila Bueno
哦,我花了两个小时试图找出问题出在哪里,谢谢。 - joghm
我使用了 SingleEventLiveData 而不是 MutableLiveData,问题已经解决。 - joghm
LiveData和Navigation的作者已经多次表示,SingleLiveEvent实际上是一种反模式,因为它滥用了LiveData。https://www.reddit.com/r/androiddev/comments/dz1q3f/askandroid_at_android_dev_summit_2019/ - EpicPandaForce
干得好。基本上返回按钮运作良好,但由于MutableLiveData,它会再次附加片段。你救了我的一天。 - Afzal Khan
显示剩余2条评论

10

当我使用 MutableLiveData 在多个片段之间导航并观察 live data 对象时,出现了这个问题。

我通过仅观察一次 live data 对象或使用 SingleLiveEvent 来解决它,而不是使用 MutableLiveData。因此,如果您在这种情况下遇到相同的问题,请尝试仅观察一次 live data 对象或使用 SingleLiveEvent


8
您可以使用以下内容来处理 Activity
onBackPressedDispatcher.addCallback(
                this,
                object : OnBackPressedCallback(true) {
                    override fun handleOnBackPressed() {
                        onBackPressed()
                        // if you want onBackPressed() to be called as normal afterwards

                    }
                }
        )

对于fragment,需要使用requireActivity()和Callback。
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()

11
绝对不应该在“OnBackPressedCallback”中调用“onBackPressed()” - 这肯定会导致无限循环。无论如何,“NavController”都会设置自己的“OnBackPressedCallback”,您不需要这样做。 - ianhanniballake
2
这将是必要的。为了在 onBackPressed() 上执行某个任务。 - ॐ Rakesh Kumar
@David,很高兴能帮助你。 :) - ॐ Rakesh Kumar
2
从片段中调用 requireActivity().onBackPressed() 会导致 无限循环 并使应用程序崩溃。 - Timuçin
1
补充@ianhanniballake的评论,如果你遇到了这样的情况,想要忽略onBackPress(即在返回键按下时不执行任何操作),你可以在被调用后通过添加this.remove()来移除回调函数,并将其作为高阶函数传递给父函数,以执行正常的返回行为requireActivity().onBackPressed()。这个函数可以在子片段中重写。 - Khaled Ahmed
显示剩余3条评论

3

一旦导航完成,您需要将MutableLiveData设置为null。

例如:

private val _name = MutableLiveData<String>()
val name: LiveData<String>
    get() = _name

fun printName(){
    _name.value = "John"
}
fun navigationComplete(){
    _name.value = null
}

假设您正在观察片段中的“name”并进行一些导航,一旦名称为John,应该像这样:

        viewModel.name.observe(viewLifecycleOwner, Observer { name ->
        when (name) {
            "John" -> {
                this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
                viewModel.navigationComplete()
            }          
        }
    })

现在您的返回按钮将毫无问题地工作。

有些数据几乎只使用一次,例如 snackbar 消息或导航事件,因此一旦使用完成,必须将该值设置为 null。

问题在于 _name 中的值仍然为 true,因此无法返回到上一个片段。


对我来说完美地工作了。谢谢,节省了我很多时间。 - Saljith Kj

2
如果您使用Moxy或类似的库,请查看从一个片段导航到第二个片段时的策略。当策略为AddToEndSingleStrategy时,我遇到了同样的问题。您需要将其更改为SkipStrategy
interface ZonesListView : MvpView {

    @StateStrategyType(SkipStrategy::class)
    fun navigateToChannelsList(zoneId: String, zoneName: String)
}

谢谢你的回答!不过我没有使用任何库,但我会记住这个建议。 - Christilyn Arjona

1
如果您的问题涉及片段-底部导航,则最简单的答案可能是将defaultNavHost ="false"设置为默认值。根据官方文档,假设您设置了3个用于底部导航的片段,那么将"defaultNavHost = true"设置为父级,这样当用户在第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" /> 

1

对于正在使用LiveData设置导航ID的每个人,无需使用SingleLiveEvent。您只需在设置其初始值后将destinationId设置为null即可。

例如,如果您想从Fragment A导航到B。

ViewModel A:

val destinationId = MutableLiveData<Int>()

fun onNavigateToFragmentB(){
    destinationId.value = R.id.fragmentB
    destinationId.value = null
}

这仍会触发片段中的观察器,并进行导航。
片段A
viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
    when (destinationId) {
        R.id.fragmentB -> navigateTo(destinationId)
    }
})

1
在 OnCreateView 中调用 onBackPressed。
private fun onBackPressed() {
    requireActivity().onBackPressedDispatcher.addCallback(this) {
        //Do something
    }
}

0

我曾经因为下面的“运行阻塞”代码块而遇到了同样的问题。所以如果不必要,就不要使用它。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接