39

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>
Christilyn Arjona
  • 2,173
  • 3
  • 13
  • 20
  • 1
    `this.findNavController().popBackStack()` will work for you – ॐ Rakesh Kumar Jan 21 '20 at 05:17
  • Does your `ProfileFragment` implement any [custom back navigation](https://developer.android.com/guide/navigation/navigation-custom-back#implement_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 Jan 21 '20 at 05:23
  • @ianhanniballake I do not do anything in the ProfileFragment. I've edited my question to include it – Christilyn Arjona Jan 21 '20 at 05:26
  • @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 Jan 21 '20 at 05:29
  • Can you include where you add your `NavHostFragment` to your Activity (I assume through a `` tag in your layout XML)? Does the back button work on all of the other destinations you have? – ianhanniballake Jan 21 '20 at 05:37
  • @ChristilynArjona, Did you pass an `action` with `ProfileFragment` like this `app:destination="@id/dashboardFragment"` – ॐ Rakesh Kumar Jan 21 '20 at 05:40
  • @ianhanniballake Yea it's through a tag. I've added my xml code. Also, the other fragments works fine. Pressing back in ProfileFragment specifically for some reason is not going back. – Christilyn Arjona Jan 21 '20 at 15:00
  • @ChristilynArjona Did you solve problem? I have the same one... What is the reason here??? – Bulma May 05 '20 at 11:38
  • @HoangVu Unforetunately no. I just ended up using deep links and navigating to it using the link uri. – Christilyn Arjona May 08 '20 at 16:46
  • @ChristilynArjona see the below, hope it will help you. Thanks https://stackoverflow.com/questions/59834398/android-navigation-component-back-button-not-working/62732296#62732296 – Shah91n Jul 04 '20 at 17:31
  • Any solutions on this one? I have an almost similar configuration, `BottonNavigationView` with 4 tabs, I use the latest androidx-fragment alpha which supports multiple BackStacks. All other tabs are navigating in their own backstacks properly, but there's one of them, which is denying to go back at all! `popBackstack`, `navigateUp` and pressing back button, nothing works. Should I post a new SO question with my setup? – Kshitij Patil Oct 22 '21 at 08:03
  • @KshitijPatil Yea it would be best to post a new one. This one is a little outdated and I haven't kept track of the project that was associated with this code. A hacky approach you can try would be probably to try and deep-linking to the new fragment instead – Christilyn Arjona Oct 24 '21 at 20:45

11 Answers11

64

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()
}
}
nirazverma
  • 1,001
  • 10
  • 15
45

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)
}
trOnk12
  • 713
  • 6
  • 10
10

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.

8

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()
ॐ Rakesh Kumar
  • 1,318
  • 1
  • 14
  • 24
  • 11
    You absolutely should not be calling `onBackPressed()` from an `OnBackPressedCallback` - that will most certainly lead to an infinite loop. `NavController` sets up its own `OnBackPressedCallback` anyways, you don't need to do this. – ianhanniballake Jan 21 '20 at 05:39
  • 2
    This will be needed. To perform a certain task on `onBackPressed()` – ॐ Rakesh Kumar Jan 21 '20 at 05:41
  • @RakeshKumar Thanks for the answer! I tried your method but unfortunately it does not work. Navigating to the other fragments causes the app to crash. – Christilyn Arjona Jan 21 '20 at 15:02
  • @ChristilynArjona, Can you update crash `Log` stack track – ॐ Rakesh Kumar Jan 21 '20 at 16:39
  • @David, Glad to help you. :) – ॐ Rakesh Kumar May 15 '20 at 10:01
  • @ianhanniballake that happened to me. Until I read your comment so thanks. I'm just lost in handling the back button. Basically I don't want users to navigate back to the auth screens unless then have signed out in the settings. Any back button should just go to the previous screen. That's all I'm looking for. Any suggestions? – wesley franks Jun 06 '20 at 17:36
  • 2
    Calling `requireActivity().onBackPressed()` from fragment results in `infinite loop` and the app crashes. – Timuçin Dec 06 '20 at 15:18
  • 1
    adding to @ianhanniballake comment, if you ever ran to a usecase where you want to ignore the onBackPress (i.e do nothing on back press), you can remove the callback after being called by adding `this.remove()` and adding it again by passing the same callBack as a high order function to a parent function that does the normal back behavior `requireActivity().onBackPressed()` that can be overriden in child fragments – Khaled Ahmed Dec 13 '20 at 11:26
3

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.

Shah91n
  • 432
  • 1
  • 4
  • 10
2

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)
}
NathanByel
  • 29
  • 2
1

Call onBackPressed in OnCreateView

private fun onBackPressed() {
    requireActivity().onBackPressedDispatcher.addCallback(this) {
        //Do something
    }
}
Dino Sunny
  • 921
  • 1
  • 10
  • 18
1

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)
    }
})
Paul Nitu
  • 126
  • 1
  • 8
1

The Simplest Answer for your problem (If it has something to do with fragments - Bottom navigation) could be

To set defaultNavHost = "false"

From Official Documentation it says-> Let's say you have 3 fragments set for Bottom Navigation, then setting

"defaultNavHost = true" will make fragment A acts like a parent, so when user clicks on back button in fragment 3 , it comes to fragment 1 instead of closing the activity (Bottom Navigation as Example).

Your XML should look like this, if you wanna just press back and close the activity from any fragment you are in.

   <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" /> 
Nithin B
  • 61
  • 3
0

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()
    }
rugue
  • 23
  • 6
0

I had faced the same issue due to the below "run blocking" code block. So don't use it if not necessary.

enter image description here

Tyler2P
  • 2,324
  • 26
  • 22
  • 31