Richard Wilson
Richard Wilson

Reputation: 299

App crashes when tapping multiple times on Bottom Navigation Views

My app has an activity that hosts 3 fragments. These fragments can be navigated by tapping on the bottom navigation views. It works quite fine only that when I tried tapping on the bottom navigation views severally, it crashed with the following error at runtime:

java.lang.IllegalArgumentException: saveBackStack("48c3d9bf-beff-4ec0-8a1b-fb91b56a2765") must be self contained and not reference fragments from non-saved FragmentTransactions. Found reference to fragment SecondFragment{57f9be2} (dd3744e7-8aa3-4c45-b6bc-312a9d46afb4 id=0x7f0a00b0) in BackStackEntry{ba06b73 48c3d9bf-beff-4ec0-8a1b-fb91b56a2765} that were previously added to the FragmentManager through a separate FragmentTransaction.
        at androidx.fragment.app.FragmentManager.saveBackStackState(FragmentManager.java:2052)
        at androidx.fragment.app.FragmentManager$SaveBackStackState.generateOps(FragmentManager.java:3172)
        at androidx.fragment.app.FragmentManager.generateOpsForPendingActions(FragmentManager.java:1953)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1643)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:480)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6819)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)

I've checked throughout this site and several other sites for a solution to the issue but found none. So i would like if anyone could help out.

Here's my current activity's code:

public class HomeActivity extends AppCompatActivity {
    private DrawerLayout drawer;
    // Last update time, click sound, search button, search panel.
    TextView time_field;
    MediaPlayer player;
    ImageView Search;
    EditText textfield;
    // For scheduling background image change(using constraint layout, start counting from dubai, down to statue of liberty.
    ConstraintLayout constraintLayout;
    public static int count = 0;
    int[] drawable = new int[]{R.drawable.dubai, R.drawable.central_bank_of_nigeria, R.drawable.eiffel_tower, R.drawable.hong_kong, R.drawable.statue_of_liberty};
    Timer _t;

    private WeatherDataViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        // use home activity layout.

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        // Allow activity to make use of the toolbar

        drawer = findViewById(R.id.drawer_layout);

        viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);

        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar
                , R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer.addDrawerListener(toggle);
        toggle.syncState();

        time_field = findViewById(R.id.textView9);
        Search = findViewById(R.id.imageView4);
        textfield = findViewById(R.id.textfield);
        //  find the id's of specific variables.

        BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
        // host 3 fragments along with bottom navigation.
        final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
        assert navHostFragment != null;
        final NavController navController = navHostFragment.getNavController();
        NavigationUI.setupWithNavController(bottomNavigationView, navController);

        // For scheduling background image change
        constraintLayout = findViewById(R.id.layout);
        constraintLayout.setBackgroundResource(R.drawable.dubai);
        _t = new Timer();
        _t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                // run on ui thread
                runOnUiThread(() -> {
                    if (count < drawable.length) {

                        constraintLayout.setBackgroundResource(drawable[count]);
                        count = (count + 1) % drawable.length;
                    }
                });
            }
        }, 5000, 5000);

        Search.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // make click sound when search button is clicked.
                player = MediaPlayer.create(HomeActivity.this, R.raw.click);
                player.start();

                getWeatherData(textfield.getText().toString().trim());
                // make use of some fragment's data

                Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
                if (currentFragment instanceof FirstFragment) {
                    FirstFragment firstFragment = (FirstFragment) currentFragment;
                    firstFragment.getWeatherData(textfield.getText().toString().trim());
                } else if (currentFragment instanceof SecondFragment) {
                    SecondFragment secondFragment = (SecondFragment) currentFragment;
                    secondFragment.getWeatherData(textfield.getText().toString().trim());
                } else if (currentFragment instanceof ThirdFragment) {
                    ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
                    thirdFragment.getWeatherData(textfield.getText().toString().trim());
                }
            }

            private void getWeatherData(String name) {

                ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);

                Call<Example> call = apiInterface.getWeatherData(name);

                call.enqueue(new Callback<Example>() {
                    @Override
                    public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {

                        try {
                            assert response.body() != null;
                            time_field.setVisibility(View.VISIBLE);
                            time_field.setText("First Updated:" + " " + response.body().getDt());
                        } catch (Exception e) {
                            time_field.setVisibility(View.GONE);
                            time_field.setText("First Updated: Unknown");
                            Log.e("TAG", "No City found");
                            Toast.makeText(HomeActivity.this, "No City found", Toast.LENGTH_SHORT).show();
                        }
                    }

                    @Override
                    public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
                        t.printStackTrace();
                    }

                });
            }

        });
    }
}

EDIT

Second Fragment:

public class SecondFragment extends Fragment {

    // TODO: Rename parameter arguments, choose names that match
    // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
    private static final String ARG_PARAM1 = "param1";
    private static final String ARG_PARAM2 = "param2";

    // TODO: Rename and change types of parameters
    private String mParam1;
    private String mParam2;

    public SecondFragment() {
        // Required empty public constructor
    }

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param param1 Parameter 1.
     * @param param2 Parameter 2.
     * @return A new instance of fragment SecondFragment.
     */
    // TODO: Rename and change types and number of parameters
    public static SecondFragment newInstance(String param1, String param2) {
        SecondFragment fragment = new SecondFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_second, container, false);
    }

    public void getWeatherData(String trim) {
    }
}

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/my_nav"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.wiz.lightweatherforecast.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" />
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.wiz.lightweatherforecast.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
    <fragment
        android:id="@+id/thirdFragment"
        android:name="com.com.wiz.lightweatherforecast.ThirdFragment"
        android:label="fragment_third"
        tools:layout="@layout/fragment_third" />
</navigation>

I navigate through fragments by clicking on the bottomnavviews(illustrated with red ticks): https://i.sstatic.net/ScFW6.jpg making use of this dependency; implementation "androidx.navigation:navigation-fragment:2.4.0-alpha01"

Upvotes: 7

Views: 2390

Answers (4)

zaheer ahmed
zaheer ahmed

Reputation: 198

using NavigationUI set the third parameter(saveState) false in setupWithNavController method that will fix this crash.

NavigationUI.setupWithNavController(bottomNavigationView, navController,false)

Upvotes: 1

Zain
Zain

Reputation: 40830

java.lang.IllegalArgumentException: saveBackStack must be self contained and not reference fragments from non-saved FragmentTransactions. Found reference to fragment SecondFragment in BackStackEntry that were previously added to the FragmentManager through a separate FragmentTransaction.

Trying to understand that:

  • When you try to navigate to the SecondFragment from the BottomNavigationView; it says that this particular fragment (SecondFragment) already exists in the Back Stack with an old / separate FragmentTransaction; so it's wrong to re-add it to the back stack. >> Instead it should be reused.

    This means that instead of doing: fm.beginTransaction().hide(currentFragment).show(newFragment).commit()

    It does instead: fm.beginTransaction().add(R.id.frag_container, newFragment, tag).commit()

  • When repeatedly tapping on the BottomNavView, for some reason it reaches to a non-saved fragmentTransaction (non-committed); and just after that when you try to navigate to another fragment, it says "You should not reference a new fragment from the current uncommitted transaction".

Now try to resolve this when item is (re-selected), by poping the back stack when an item is re-selected in the BottonNavView by setting OnItemReselectedListener:

BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
// host 3 fragments along with bottom navigation.
final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
assert navHostFragment != null;
final NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNavigationView, navController);

bottomNavigationView.setOnItemReselectedListener((BottomNavigationView.OnNavigationItemReselectedListener)
        item -> navController.popBackStack(item.getItemId(), false)
);  

Upvotes: 0

quealegriamasalegre
quealegriamasalegre

Reputation: 3258

UPDATE

I think the issue is that you are adding Stuff to the BackStack that is already there. I still dont see the full picture as I dont know exactly how your NavController performs backStack operations but I think we can make the exception go away adding the following to your code:

first you need to add a listener to your bottomNavigationView and then we pop the backstack whenever a new item is clicked. It should work if you just add the following code to your activity onCreate() method.

bottomNavigationView.setOnNavigationItemSelectedListener(new NavigationView
                .OnNavigationItemSelectedListener() {
            @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                
              if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
                  getSupportFragmentManager().popBackStack();
              }
            }
        }); 

this will admittedly mess up your back navigation cause you are telling android to forget past fragments whenever you click a new one. In other words presseing the backbutton will show you your past activity instead of your past fragment. But it should allow you to press any navigation item repeatedly without throwing the exception.

There are also ways to do this while preserving back navigation but I think it is best if you try this out first and see if it fixes the issue before proseding.

OLD ANSWER

Im writing this as an answer as it would probably be too long for a comment. it is not a proper answer as I we would need to see more of your code for that:

the exception is telling you that there is an issue with your backstack of fragments (this is basically just the place where android remembers and stores the state of your past activities and fragments so that you see the same thing you saw before once you press the back button). I cannot tell for sure what the issue is as I dont see your fragment classes but it sounds like in your code there might be some sort of circular reference or smtng like that. Maybe add code from your fragments. In your position I would look at the fragment called SecondFragment that is references in the exception and in particular its saveInstanceState method. not sure if somehow artificially making your fragments singleTask or singleInstance could help. I recommend reading up on the BackStack. The following docs on a new FragmentManager relese seem to touch on your issue

Upvotes: 0

Darkman
Darkman

Reputation: 2981

As you might have guessed already, the problem was about a race condition between the older fragment and the newer one. So here's what you can do.

static volatile - A gate keeper. This will ignore the newer ones if the older has not yet finish.

private static volatile boolean isClicking = false;

...
Search.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v)
                {
                    if(!isClicking) {
                        isClicking = true;

                        // make click sound when search button is clicked.
                        player = MediaPlayer.create(HomeActivity.this, R.raw.click);
                        player.start();

                        getWeatherData(textfield.getText().toString().trim());
                        // make use of some fragment's data

                        Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
                        if(currentFragment instanceof FirstFragment) {
                            FirstFragment firstFragment = (FirstFragment) currentFragment;
                            firstFragment.getWeatherData(textfield.getText().toString().trim());
                        } else if(currentFragment instanceof SecondFragment) {
                            SecondFragment secondFragment = (SecondFragment) currentFragment;
                            secondFragment.getWeatherData(textfield.getText().toString().trim());
                        } else if(currentFragment instanceof ThirdFragment) {
                            ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
                            thirdFragment.getWeatherData(textfield.getText().toString().trim());
                        }
                        
                        isClicking = false;
                    }
                }
            });
...

static synchronized - Create a helper function that calls everything in onClick(). This will queue the tasks.

...
Search.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        syncOnClick();
    }
});
...

//Outside of "onCreate()".
private static synchronized void syncOnClick()
{
    // make click sound when search button is clicked.
    player = MediaPlayer.create(HomeActivity.this, R.raw.click);
    player.start();

    getWeatherData(textfield.getText().toString().trim());
    // make use of some fragment's data

    Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
    if(currentFragment instanceof FirstFragment) {
        FirstFragment firstFragment = (FirstFragment) currentFragment;
        firstFragment.getWeatherData(textfield.getText().toString().trim());
    } else if(currentFragment instanceof SecondFragment) {
        SecondFragment secondFragment = (SecondFragment) currentFragment;
        secondFragment.getWeatherData(textfield.getText().toString().trim());
    } else if(currentFragment instanceof ThirdFragment) {
        ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
        thirdFragment.getWeatherData(textfield.getText().toString().trim());
    }
}

Upvotes: 0

Related Questions