17
votes

I'm using the Navigation Component in android where I have set 6 fragments initially. The problem is when I added a new fragment (ProfileFragment).

When I navigate to this new fragment from the start destination, pressing the native back button does not pop the current fragment off. Instead, it just stays to the fragment I'm in - the back button does nothing.

Here's my 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>

enter image description here

In the image above, the highlighted arrow (in the left) is the navigation action I'm having troubles with.

In my Fragment code, I'm navigating as follows:

findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)

The other navigation actions are working as intended. But for some reason, this newly added fragment does not behave as intended.

There are no logs showing when I navigate to ProfileFragment and when I press the back button.

Am I missing something? or is there anything wrong with my action/fragment configurations?

EDIT: I do not do anything in ProfileFragment. Here's the code for it:

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


}

And my activity xml containing the nav host:

<?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>
9
this.findNavController().popBackStack() will work for youॐ Rakesh Kumar
Does your ProfileFragment implement any custom back navigation? Any custom back code you do there will take precedence over the NavController. Can you include the code for that fragment?ianhanniballake
@ianhanniballake I do not do anything in the ProfileFragment. I've edited my question to include itChristilyn Arjona
@RakeshKumar I'm sorry, I'm not completely understanding what you mean. Where do I add this.findNavController().popBackStack() ? and my dashboard action already have destination as profileFragment.Christilyn Arjona
Can you include where you add your NavHostFragment to your Activity (I assume through a <fragment> tag in your layout XML)? Does the back button work on all of the other destinations you have?ianhanniballake

9 Answers

29
votes

For anyone using LiveData in a previous Fragment which is a Home Fragment, whenever you go back to the previous Fragment by pressing back button the Fragment is starting to observe the data and because ViewModel survives this operation it immediately emits the last emitted value which in my case opens the Fragment from which I pressed the back button, that way it looks like the back button is not working the solution for this is using something that emits data only once. I used this :

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

if you are using setupActionBarWithNavController in Navigation Component such as:

 setupActionBarWithNavController(findNavController(R.id.fragment))

then also override and config this methods in your main activity:

 override fun onSupportNavigateUp(): Boolean {
    val navController = findNavController(R.id.fragment)
    return navController.navigateUp() || super.onSupportNavigateUp()
}

My 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()
}
}
9
votes

You can use this following for the Activity

onBackPressedDispatcher.addCallback(
                this,
                object : OnBackPressedCallback(true) {
                    override fun handleOnBackPressed() {
                        onBackPressed()
                        // if you want onBackPressed() to be called as normal afterwards

                    }
                }
        )

For the fragment, It will be needed requireActivity() along with Callback

requireActivity().onBackPressedDispatcher.addCallback(
                    this,
                    object : OnBackPressedCallback(true) {
                        override fun handleOnBackPressed() {
                            requireActivity().onBackPressed()
                            // if you want onBackPressed() to be called as normal afterwards

                        }
                    }
            )

If you have a Button or something else to perform an action then you can use

this.findNavController().popBackStack()
6
votes

This problem happened to me while using MutableLiveData to navigate between fragments and was observing the live data object at more than one fragment.

I solved it by observing the live data object one time only or by using SingleLiveEvent instead of MutableLiveData. So If you're having the same scenario here, try to observe the live data object one time only or use SingleLiveEvent.

2
votes

You need to set the MutableLiveData to null once the navigation is done.

For example

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

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

Now say you are observing the "name" in your fragment and you are doing some navigation once the name is John then should be like that:

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

Now your back button will be working without a single problem.

Some data are almost used only once, like a Snackbar message or navigation event therefore you must tell set the value to null once done used.

The problem is that the value in _name remains true and it’s not possible to go back to previous fragment.

1
votes

If you use Moxy or similar libs, checkout the strategy when you navigate from one fragment to second. I had the same issue when strategy was AddToEndSingleStrategy. You need change it to SkipStrategy.

interface ZonesListView : MvpView {

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

Call onBackPressed in OnCreateView

private fun onBackPressed() {
    requireActivity().onBackPressedDispatcher.addCallback(this) {
        //Do something
    }
}
1
votes

For everyone who is using LiveData for setting navigation ids, there's no need to use SingleLiveEvent. You can just set the destinationId as null after you set its initial value.

For instance if you want to navigate from Fragment A to B.

ViewModel A:

val destinationId = MutableLiveData<Int>()

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

This will still trigger the Observer in the Fragment and will do the navigation.

Fragment A

viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
    when (destinationId) {
        R.id.fragmentB -> navigateTo(destinationId)
    }
})
0
votes

Set the MutableLiveData to false after navigation

Put this code in your ViewModel.kt

private val _eventNextFragment = MutableLiveData<Boolean>()
    val eventNextFragment: LiveData<Boolean>
        get() = _eventNextFragment

    fun onNextFragment() {
        _eventNextFragment.value = true
    }

    fun onNextFragmentComplete(){
        _eventNextFragment.value = false
    }

Let's say you want to navigate to another fragment, you'll call the onNextFragmentComplete method from the viewModel immediately after navigating action.

Put this code in your Fragment.kt

private fun nextFragment() {
        val action = actionFirstFragmentToSecondFragment()
        NavHostFragment.findNavController(this).navigate(action)
        viewModel.onNextFragmentComplete()
    }