android – Backstack management : Restarter must be created only during owner's initialization stage-ThrowExceptions

Exception or error:

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

How to solve:

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).

###

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。

###

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.

###

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

###

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.

###

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 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).

###

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());
    }
}

###

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.

###

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();

Leave a Reply

Your email address will not be published. Required fields are marked *