24

I am using a bottom navigation bar in my MainActivity to handle some fragments. This is the code used for switching between them:

private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
    if (item.isChecked &&
        supportFragmentManager.findFragmentById(R.id.act_main_fragment_container) != null
    )
        return@OnNavigationItemSelectedListener false
    val fragment =
        when (item.itemId) {
            R.id.navigation_home      -> fragments[0]
            R.id.navigation_bookings  -> fragments[1]
            R.id.navigation_messages  -> fragments[2]
            R.id.navigation_dashboard -> fragments[3]
            R.id.navigation_profile   -> fragments[4]
            else                      -> fragments[0]
        }
    this replaceWithNoBackStack fragment
    return@OnNavigationItemSelectedListener true
}

the method replaceWithNoBackstack is just a short-hand for this:

supportFragmentManager
    ?.beginTransaction()
    ?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
    ?.replace(containerId, fragment)
    ?.commit()

The problem is that when i switch faster between them, my app crashes with the following exception:

java.lang.IllegalStateException: Restarter must be created only during owner's initialization stage at androidx.savedstate.SavedStateRegistryController.performRestore(SavedStateRegistryController.java:59) at androidx.fragment.app.Fragment.performCreate(Fragment.java:2580) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:837) at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1237) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1302) at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439) at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2075) at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1865) at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1820) at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1726) at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6709) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769) I've been searching a lot and couldn't find an answer.

I also got this error if I do an API call, put the app in background, wait for the response, and at the time I go back to the app, the app crashes because I am trying to display a dialog fragment immediately (the reason I think this is happening is that the transaction of recreating the fragment when coming back from the background is still in progress at the time of displaying the dialog fragment). I solved this in a hacky way by setting a 500ms delay for the dialog because I couldn't figure out other solutions.

Please ask if you need more details regarding this. Thank you in advance!

POSSIBLE TEMP SOLUTIONS

EDIT I solved this issue by downgrading the app compat depedency to androidx.appcompat:appcompat:1.0.2 but this is just a temporary solution, since i will have to update it in future. I'm hoping someone will figure it out.

EDIT 2 I solved the issue by removing setTransition() from fragment transactions. At least I know the reason why android apps does not have good transitions in general

EDIT 3 Maybe the best solution to avoid this issue and also make things work smoothly is just to use ViewPager to handle bottom bar navigation

Gabi
  • 667
  • 1
  • 7
  • 17

11 Answers11

10

because the version 1.0.0 has not check the state, so it will not throw the exception, but the version 1.1.0 changes the source code,so it throws the exception.

this is the Fragment version-1.1.0 source code, it will invoke the method performRestore

    void performCreate(Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mState = CREATED;
        mCalled = false;
        mSavedStateRegistryController.performRestore(savedInstanceState);
        onCreate(savedInstanceState);
        mIsCreated = true;
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onCreate()");
        }
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    }

/**
the exception
**/
public void performRestore(@Nullable Bundle savedState) {
        Lifecycle lifecycle = mOwner.getLifecycle();
        if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
            throw new IllegalStateException("Restarter must be created only during "
                    + "owner's initialization stage");
        }
        lifecycle.addObserver(new Recreator(mOwner));
        mRegistry.performRestore(lifecycle, savedState);
    }

this is the version-1.0.0 source code,did not invoke the performRestore,so will not throw the exception

void performCreate(Bundle savedInstanceState) {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
    }
    mState = CREATED;
    mCalled = false;
    onCreate(savedInstanceState);
    mIsCreated = true;
    if (!mCalled) {
        throw new SuperNotCalledException("Fragment " + this
                + " did not call through to super.onCreate()");
    }
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}

There are two different solution which can handle this:
The first solution is to split the transaction。
Because we always use replace or merge remove and add into one Transaction. We can split the transaction to two transaction like this:

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        //commit immediately
            ft.remove(prev).commitAllowingStateLoss();
        }
        FragmentTransaction addTransaction = manager.beginTransaction();
        addTransaction.addToBackStack(null);
        addTransaction.add(layoutId, fragment,
                tag).commitAllowingStateLoss();

because this two transaction will be two different Message which will be handled by Handler.
The second solution is check the state in advance. we can follow the source code,check the state in advance

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        if (prev.getLifecycle().getCurrentState() != Lifecycle.State.INITIALIZED) {
            return;
        }
            ft.remove(prev);
        }

I recommend the first way,because the second way is folowing the source code,if the source code change the code, it will be invalid。

Drown Coder
  • 189
  • 1
  • 8
  • Then what is the best way of handling this? I realised this happens when you begin a transaction while another transaction is already happening, so when the user opens a fragment and then hits back immediately it crashes. – Roudi Jul 10 '19 at 23:29
  • @PampaZiya I have edited the answer, adding the solution – Drown Coder Jul 11 '19 at 02:18
  • 2
    but what if you want to animate the removal and addition ? – Roudi Jul 11 '19 at 02:43
5

I had the same problem.

val fragment = Account.activityAfterLogin
        val ft = activity?.getSupportFragmentManager()?.beginTransaction()
        //error
        ft?.setCustomAnimations(android.R.anim.slide_in_left,android.R.anim.slide_out_right)0
        ft?.replace(R.id.framelayout_account,fragment)
        ft?.commit()

Changing the library version did not help. I solved this by adding the ft?.AddToBackStack(null) line after the ft?.setCustomAnimations () method and that’s it. Animation works and there are no crashes.

Felix Gerber
  • 1,615
  • 3
  • 30
  • 40
Mako Storm
  • 67
  • 1
  • 2
  • I have the same problem the app crashes when I try to use animations is there any alternative to ft?.AddToBackStack(null) ?? – vinay kumar Jan 08 '20 at 22:26
  • This was the only way to get it to work when using `setCustomAnimations()`! My app was crashing when a new FragmentTransaction was triggered before the previous animation wasn't finished. Spent hours on this, thanks. Still strange how and why this solves the problem. – BenjyTec Feb 18 '20 at 11:58
  • Indeed this is also my only solution. But this would add a history entry for fragments added. Do i really have to override the onBackPress and finish the activity manually? – Hatzen Aug 09 '20 at 21:50
3

If you're using 'androidx.core:core-ktx:1.0.2', try changing to 1.0.1

If you're using lifecycle(or rxFragment) and androidx_appcompat:alpha05, try changeing versio.
ex) appcompat : 1.1.0-beta01 or 1.0.2

I think's that it appears as an error when saving the state when the target fragment is reused (onPause-onResume).

진홍빛
  • 194
  • 4
2

I changed implementation to api for androidx.appcompat:appcompat:1.0.2 and its worked for me

2

If it can help, I have encountered the same issue with a BottomNavigationView and setCustomAnimations, basically by switching quickly between Fragments, you may end up starting a FragmentTransaction while the previous one has not finished and then it crashes.

To avoid that, I disable the Navigation Bar until the transition is finished. So I have created a method to enable/disable the BottomNavigationView items (disabling the BottomNavigationView itself does not disable the menu or I didn't find the way) and then I re-enable them once the transition is completed.

To disable the items I call the following method right before starting a FragmentTransition:

public void toggleNavigationBarItems(boolean enabled) {
   Menu navMenu = navigationView.getMenu();
   for (int i = 0; i < navMenu.size(); ++i) {
       navMenu.getItem(i).setEnabled(enabled);
   }
}

To re-enable them, I have created an abstract Fragment class for the Fragments loaded from the BottomNavigationView. In this class, I overrides onCreateAnimator (if you use View Animation you should override onCreateAnimation) and I re-enable them onAnimationEnd.

@Nullable
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
    if(enter){ // check the note below
        Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                myActivity.toggleNavigationBarItems(true)
            }
        });
        return animator;
    }
    return super.onCreateAnimator(transit, enter, nextAnim);
}

Note: as my enter and exit animations have the same duration, I don't need to synchronise them as the enter animation starts after the exit one. That's why the if (enter) is sufficient.

2

I fixed this problem with add 'synchronized' into add fragment method

before :

public void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}

after :

public synchronized void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}
Mahdi Zareei
  • 1,299
  • 11
  • 18
1

This bug seems to be resolved using androidx.appcompat:appcomat:1.1.0-rc01 and androidx.fragment:fragment:1.1.0-rc03

https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-rc03

  • I tried to update both and it did not work. If you hit back while animating to anew fragment (mid transaction), it just crashes for me. I'm not using pop, I'm just initiating a new replace transaction, which results in the same error – Roudi Jul 26 '19 at 01:02
  • Yes thats correct this is still happening in certain cases in rc03 :( – Geoffrey Powell Jul 30 '19 at 22:03
  • Yes this has not been fixed even with 1.2.0-alpha01. I wonder then what's the best way to approach this ? – Roudi Aug 08 '19 at 06:31
  • @PampaZiya If you have a way to reproduce the problem, can you post a sample project in [issues tracker](https://issuetracker.google.com/issues/136022199)? Some of us see it only in Google Play tracker and it requires a perfect timing which is not reproducible. – Dmitry Dec 24 '19 at 06:48
  • @Dmitry You can reproduce by having animation between fragments and hitting the back button while the fragment is animating in. – Roudi Dec 24 '19 at 09:30
1

I have this issue when using setCustomAnimations. by removing setCustomAnimations solved my problem. also I have no problem when I create new instance of fragment before showing it even using setCustomAnimation.

EDIT: another way is adding fragment to backstack.

Hadi Ahmadi
  • 1,924
  • 2
  • 17
  • 38
1

I was able to fix this (hopefully ) by using commitNow() instead of commit() for all bottom nav fragment transactions. I like this approach better as it allows you to still use custom transitions between fragments.

Note: This is a solution only if you don't want your bottom nav transactions to be added to backstack (which you should not be doing anyways).

simekadam
  • 7,334
  • 11
  • 56
  • 79
1

Nothing worked except Drown Coder's solution, but it was still not perfect, because it adds transactions to backstack. So if you press all buttons in bottom navigation, you have at least 1 of every fragment in backstack. I slightly improved this solution, so you don't use .replace() that crashes app whith thansaction animations.

Here is the code:

if (getChildFragmentManager().getBackStackEntryCount() > 0) {
    getChildFragmentManager().popBackStack();
}

FragmentTransaction addTransaction = getChildFragmentManager().beginTransaction();
addTransaction.setCustomAnimations(R.animator.fragment_fade_in, R.animator.fragment_fade_out);
addTransaction.addToBackStack(null);
addTransaction.add(R.id.frame, fragment, fragment.getClass().getName()).commitAllowingStateLoss();
0

I found another way of creating this case.

CASE-1

  1. Inflate a fragment in frame-layout at an activity
  2. start an API request (don't consume the api response when app in foreground)
  3. Keep your app in background
  4. Consume the API request (suppose you want to add another fragment on api response)
  5. Inflate another fragment using .replace() method on the same frame-layout
  6. You will be able to create the Crash

CASE-2

  1. Inflate a fragment in frame-layout at an activity
  2. Start an API request
  3. Consume the api in foreground (suppose you want to add another fragment on api response, using .replace() method of fragment-manager)
  4. Put your app in background
  5. Recreate your application (you can do this using "Don't keep activities", changing permission, changing system language)
  6. Come back to your application
  7. Your activity will start re-creating
  8. Activity will auto recreate its already inflated fragment suppose it is of (point-1)
  9. Make sure API is request again in on recreate case, after point-8
  10. Consume API response and inflate another fragment using .replace() method
  11. You will be able to create the Crash (As in this case, already a transition is running point-8, and you are adding another fragment at point-10)