Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backstack management : Restarter must be created only during owner's initialization stage

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

like image 357
Gabi Avatar asked Jun 11 '19 08:06

Gabi


4 Answers

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。

like image 174
Drown Coder Avatar answered Oct 20 '22 22:10

Drown Coder


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.

like image 20
Mako Storm Avatar answered Oct 20 '22 22:10

Mako Storm


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

like image 3
진홍빛 Avatar answered Oct 20 '22 21:10

진홍빛


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

like image 2
Vlad Yatsenko Avatar answered Oct 20 '22 20:10

Vlad Yatsenko