Search code examples
androidandroid-fragmentsandroid-actionbarandroid-jetpack-navigation

Code to replace or remove existing fragment on backstack not working


I'm using the Actionbar and I have an icon in the appbar which when clicked should go to CategoryFragment. But I want to make it so that if that fragment is already on the backstack, it should either remove it and then add a new instance of CategoryFragment to the backstack or replace it.

I tried the code below, but it does not work and only overlays/superimposes another copy of CategoryFragment on top of the existing CategoryFragment and also produces very odd and unexpected behaviors. I'm not using the navcontroller to navigate to CategoryFragment since it adds another copy of the fragment in the backstack. How can I make this work?

onOptionsItemSelected() in Main Activity:

override fun onOptionsItemSelected(item: MenuItem): Boolean {

        Log.i("Lifecycle-Activity", "OnOptionsItemSelected() called")

         when (item.itemId) {
                android.R.id.home -> {
                        onBackPressed()
                        return true
                }

               R.id.categoryFragment -> {

                    var backStateName = CategoryFragment::class.simpleName

                    var fragmentPopped = supportFragmentManager.popBackStackImmediate(backStateName, 0)

                    if(!fragmentPopped)
                    {
                        var ft = supportFragmentManager.beginTransaction()

                        //also tried R.id.main_activity_container which is not working
                        ft.replace(R.id.navHostFragment, CategoryFragment())

                        ft.addToBackStack(backStateName)
                        ft.commit()
                    }

                    return true
                }
    return true
    }

Main Activity

  class MainActivity : AppCompatActivity() {

        private lateinit var appBarConfiguration: AppBarConfiguration

        private lateinit var navController: NavController

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.main_activity)

            Log.i("Lifecycle-Activity", "OnCreate() called")

            navController = Navigation.findNavController(this, R.id.navHostFragment)

            NavigationUI.setupActionBarWithNavController(this, navController)

            appBarConfiguration = AppBarConfiguration.Builder(navController.graph)
                .build()
        }

        override fun onSupportNavigateUp(): Boolean {

    super.onSupportNavigateUp()

            return NavigationUI.navigateUp(navController, appBarConfiguration)

        }

        override fun onCreateOptionsMenu(menu: Menu?): Boolean {

            Log.i("Lifecycle-Activity", "OnCreateOptionsMenu() called")

            menuInflater.inflate(R.menu.main_menu, menu)
            return super.onCreateOptionsMenu(menu)
        }

        override fun onOptionsItemSelected(item: MenuItem): Boolean {

            Log.i("Lifecycle-Activity", "OnOptionsItemSelected() called")
 when (item.itemId) {
            android.R.id.home -> {
                    onBackPressed()
                    return true
            }

  R.id.categoryFragment -> {

                var backStateName = CategoryFragment::class.simpleName

                var fragmentPopped = supportFragmentManager.popBackStackImmediate(backStateName, 0)

                if(!fragmentPopped)
                {
                    var ft = supportFragmentManager.beginTransaction()

                    //also tried R.id.main_activity_container which is not working
                    ft.replace(R.id.navHostFragment, CategoryFragment())

                    ft.addToBackStack(backStateName)
                    ft.commit()
                }

            }

           return true
        }
return true
}

Navgraph:

<?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/nav_graph_main"
    app:startDestination="@id/categoriesFragment">
    <fragment
        android:id="@+id/clockFragment"
        android:name="com.example.pomoplay.ui.main.ClockFragment"
        android:label="Pomo Clock"
        tools:layout="@layout/fragment_clock" />
    <fragment
        android:id="@+id/categoryFragment"
        android:name="com.example.pomoplay.ui.main.CategoryFragment"
        android:label="Category"
        tools:layout="@layout/fragment_category">
        <action
            android:id="@+id/action_open_category_fragment"
            app:destination="@id/categoryFragment"
            app:popUpTo="@id/categoryFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_categoryFragment_to_clockFragment"
            app:destination="@id/clockFragment" />
        <action
            android:id="@+id/action_categoryFragment_to_newTaskDialogFragment"
            app:destination="@id/newTaskDialogFragment" />
        <argument
            android:name="category"
            app:argType="com.example.pomoplay.Category" />
    </fragment>
    <fragment
        android:id="@+id/categoriesFragment"
        android:name="com.example.pomoplay.ui.main.CategoriesFragment"
        android:label="Categories"
        tools:layout="@layout/fragment_categories">
        <action
            android:id="@+id/action_categoriesFragment_to_newCategoryDialogFragment"
            app:destination="@id/newCategoryDialogFragment" />
        <argument
            android:name="category"
            app:argType="com.example.pomoplay.Category" />
        <action
            android:id="@+id/action_categoriesFragment_to_categoryFragment"
            app:destination="@id/categoryFragment" />
        <argument
            android:name="fromNewCategoryDialog"
            app:argType="boolean"
            android:defaultValue="false" />
    </fragment>
    <dialog
        android:id="@+id/newCategoryDialogFragment"
        android:name="com.example.pomoplay.ui.main.NewCategoryDialogFragment"
        tools:layout="@layout/fragment_new_category_dialog">
        <action
            android:id="@+id/action_newCategoryDialogFragment_to_categoriesFragment"
            app:destination="@id/categoriesFragment"
            app:popUpToInclusive="false" />
    </dialog>
    <dialog
        android:id="@+id/newTaskDialogFragment"
        android:name="com.example.pomoplay.ui.main.NewTaskDialogFragment"
        android:label="fragment_new_task_dialog"
        tools:layout="@layout/fragment_new_task_dialog" >
        <action
            android:id="@+id/action_newTaskDialogFragment_to_categoryFragment"
            app:destination="@id/categoryFragment"
            app:popUpTo="@+id/categoryFragment"
            app:popUpToInclusive="true" />
    </dialog>
</navigation>

Solution

  • You're using NavController and therefore you should not be doing any manual FragmentTransactions at all.

    Instead, you should Navigation to a destination, in this case, by using Navigation's support for popUpTo to pop your CategoryFragment off the stack.

    Assuming you have a destination set up in your navigation XML for your CategoryFragment with code such as:

    <fragment
        android:id="@+id/categoryFragment"
        android:name=".CategoryFragment/>
    

    You can create an action that includes the popUpTo flag you need (you'd put this directly below the <fragment> itself:

    <action
        android:id+"@+id/open_category"
        app:popUpTo="@id/categoryFragment"
        app:popUpToInclusive="true"
        app:destination="@id/categoryFragment"/>
    

    This says:

    1. Create an action named open_category

    2. When you trigger this action, pop the stack back up to categoryFragment if it already exists on the back stack (otherwise, this does nothing). The popUpToInclusive means that the previous instance is popped.

    3. After you pop the previous instance, create a new instance of categoryFragment and add it to your back stack.

    Then you can trigger that action from your onOptionsItemSelected()':

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
    
        Log.i("Lifecycle-Activity", "OnOptionsItemSelected() called")
    
        return when (item.itemId) {
            R.id.categoryFragment -> {
                // Trigger the action
                navController.navigate(R.id.open_category);
                true
           }
           default -> {
               // android.R.id.home is handled by the call to super,
               // you do not need to handle it here.
               super.onOptionsItemSelected(item)
           }
        }
    }