App crashes when tapping multiple times on Bottom Navigation Views
Asked Answered
G

4

7

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"

Gaziantep answered 17/10, 2021 at 21:4 Comment(15)
I think it may be occurs because of timer task in your activity. Try removing it. If it cause error we can fix it.Montanez
@rea ltech hacks I can't remove it, I'm using it to change my background every 5 secondsGaziantep
I'm saying it to don't remove permanently. Just To know if the error is getting because of it.Montanez
@RealTechHacks my apologies for replying late. Been really busy in college. Tried removing it, it still crashed.Gaziantep
This is a kind of problems with anonymous classes. Try putting a keyword synchronized for run() and onClick(). Or using a volatile boolean as a gate keeper e.g. volatile boolean isRunning .Bowery
Third option would be static synchronized. This should work I think.Bowery
@darkman tried all your suggestions. They all gave several errors among few are: name not allowed here, the method will return void, etc. I don't think the problem is because of the timer because even when I entirely removed the timer code, the app still crashed.Gaziantep
@Richard "They all gave several errors..." I've just tested them myself and indeed the first is only one that doesn't seem to work but others do. I'll write an answer to reflect this.Bowery
@RichardWilson please post how do you navigate between fragments, do you use traditional fragment transactions; or you use the architecture navigation components .. include code in both cases.Dazzle
@zain I use the latter. The code is included in the activity, I've as well illustrated the process using an image of the bottom nav views for easier descriptionGaziantep
@RichardWilson Sorry didn't notice it's the activity; Can you share the navigation graphDazzle
@zain Okay, I've shared itGaziantep
@RichardWilson Can you upgrade the dependency to implementation "androidx.navigation:navigation-fragment:2.4.0-alpha10" and seeDazzle
Also try the stable version implementation 'androidx.navigation:navigation-fragment:2.3.5'Dazzle
@zain i had to use the current version to fix a problem I was earlier having #67854752 & #68043091 If I upgrade/downgrade to any of the above suggested, the problem will occur againGaziantep
C
1

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

NavigationUI.setupWithNavController(bottomNavigationView, navController,false)
Cord answered 16/8, 2022 at 8:36 Comment(3)
Didn't work: Cannot resolve methodGaziantep
@RichardWilson have you updated the version of Nav Component dependency to 2.5.0?Tarshatarshish
@waheedshah Yes I did; android resource linking failed.Gaziantep
F
0

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

Foredeck answered 22/10, 2021 at 1:10 Comment(5)
Okay, I have included class secondfragmentGaziantep
Okay, I get the compile error: cannot resolve method setNavigationItemSelectedListener in bottomnavigationviewGaziantep
there should be a listener method on that view. maybe try to ise the IDE to find out its propper nameForedeck
here are the docs developer.android.com/reference/com/google/android/material/… the correct name was setOnNavigationItemSelectedListener. ill edit it in the answerForedeck
While the code ran without errors, what this code does is entirely restricts you from clicking/navigating through any fragment which wasn't what I asked. I want to still navigate through my fragments only that my app shouldn't crash if I repeatedly tap the bottom nav views. I have tested other play store apps, they don't crash meaning that's how it's meant to be. But i appreciate your help anyways only that this wasn't what i asked forGaziantep
B
0

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());
    }
}
Bowery answered 22/10, 2021 at 1:32 Comment(3)
I'm sorry for always replying late. I'm in exam week of college, so only have an hour to code a day. So I tried your suggestions and got these errors at first suggestion: modifier private static volatile not allowed here, the value assigned to isclicking is never used and cannot resolve method "getweatherdata" in homeactivity.Gaziantep
And this at the second: cannot resolve method "synconclick" in homeactivity, variable "synconclick" is never used and cannot resolve method "getweatherdata" in homeactivity.Gaziantep
For the first one -- you have to declare that variable as global (global variable). And you're getting the same error: cannot resolve method "getweatherdata" in homeactivity for both methods. The first one is basically copy-pasted of your code. So that's something on your side to figure it out.Bowery
D
0

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)
);  
Dazzle answered 24/10, 2021 at 4:56 Comment(10)
Unfortunately, it still crashes with the exact same error. And I had to change bottomNavigationView.setOnItemReselectedListener to bottomNavigationView.setOnNavigationItemReselectedListener inorder for the code to runGaziantep
@RichardWilson The problem that we have no idea how to reproduce the issue unfortuantely; so answers will be kind of a guessing game.. Try to remove this listener Search.setOnClickListener and test the tapping; will you get the same?Dazzle
Okay... I understand...now the app crashes and returns a different error(null pointer)Gaziantep
I mean if you can try to isolate the API part for a while from your app to see if this can be a cause of the problem... The other thing you can try is to run it normally but debug inside setOnItemReselectedListener callback and see if it gets called before the app crash or notDazzle
They all are interconnected. If I try to isolate the API, it will even have an error on compile time. I think the issue is entirely from the fragment/bottomnavview switching. And I have an exam tomorrow, so I'm busily reading. If not would've tried to debug inside the setOnItemReselectedListener as you suggested but time is not on my side. Even the bounty will expire tomorrowGaziantep
@RichardWilson Try this thing navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() { @Override public void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments) { navController.popBackStack(destination.getId(), false); } }); .. Hope it could workDazzle
No, it returned the same error when I triedGaziantep
Hey @RichardWilson Hope you can find a solution; I am curious to know it when you find that, and I do appreciate if you could drop me a tag uponDazzle
I postponed the issue until after my exams, then I'll try so hard to find one. Sure, I won't hesitate to tag you if I do, thanks for your concern by the wayGaziantep
Hey zain, it's been a while man. I'd appreciate it if you could please look into my new question #70455847 currently on holidaysGaziantep

© 2022 - 2024 — McMap. All rights reserved.