SMGhost
SMGhost

Reputation: 4037

Expected the adapter to be 'fresh' while restoring state

I have a viewpager2 with multiple fragments in FragmentStateAdapter. Whenever I try to open a new fragment and then go back to my current one with viewpager2, I get an exception:

Expected the adapter to be 'fresh' while restoring state.

It seems FragmentStateAdapter is unable to properly restore its state as it is expecting it to be empty.

What could I do to fix this ?

Upvotes: 20

Views: 13369

Answers (10)

Sagar G.
Sagar G.

Reputation: 534

Change your fragmentStateAdapter code from

MyPagerAdapter(childFragmentManager: FragmentManager, 
               var fragments: MutableList<Fragment>,
               lifecycle: Lifecycle
) : FragmentStateAdapter(childFragmentManager,lifecycle)

to

MyPagerAdapter(fragment: Fragment, 
               var fragments: MutableList<Fragment>
) : FragmentStateAdapter(fragment)

Note: Here we are removing lifecycle and fragmentManager dependency and fragment state gets restored on back press.

Upvotes: 0

Sandeep Dixit
Sandeep Dixit

Reputation: 1036

I got this problem after moving new SDK version. (Expected the adapter to be 'fresh' while restoring state)

android:saveEnabled="false" at ViewPager2 can be a quick fix but it may not be what you want.

<androidx.viewpager2.widget.ViewPager2    
android:saveEnabled="false"

Because this simply means your viewPager2 will always come on the first tab when your activity is recreated due to the same reason you are getting this error ( config change and activity recreate).

I wanted users to stay wherever they were So I did not choose this solution.

So I looked in my code a bit. In my case, I found a residual code from early days when I was just learning to create android app.

There was a useless call to onRestoreInstanceState() in MainActivity.onCreate, I just removed that call and it fixed my problem.

In most cases, you should not need to override these methods. If you want to override these , do not forget to call super.onSaveInstanceState / super.onRestoreInstanceState

Important Note from documentation

The default implementation takes care of most of the UI per-instance state for you by calling View.onSaveInstanceState() on each view in the hierarchy that has an id, and by saving the id of the currently focused view (all of which is restored by the default implementation of onRestoreInstanceState(Bundle)). If you override this method to save additional information not captured by each individual view, you will likely want to call through to the default implementation, otherwise be prepared to save all of the state of each view yourself.

Check if the information you want to preview is part of a view that may have an ID. Only those with an ID will be preserved automatically.

If you want to Save the attribute of the state which is not being saved already. Then you override these methods and add your bit.

protected void onSaveInstanceState (Bundle outState)

protected void onRestoreInstanceState (Bundle savedInstanceState)

In latest SDK versions Bundle parameter is not null, so onRestoreInstanceState is called only when a savedStateIsAvailable.

However, OnCreate as well gets savedState Parameter. But it can be null first time, so you need to differentiate between first call and calls later on.

Upvotes: 1

Bakyt Abdrasulov
Bakyt Abdrasulov

Reputation: 471

I've faced the same issue.

After some researching I've came to that it was related to instance of Adapter. When it is created as a lazy property of Fragment it crashes with that error.

So creating Adapter in Fragment::onViewCreated resolves it.

Upvotes: 3

Delark
Delark

Reputation: 1323

This adapter/view is useful as a replacement for FragmentStatePagerAdapter.

If what you seek is to preserve the Fragments on re-entrance from the Backstack that would be extremely difficult to achieve with this adapter.

The team placed to many breaks in place to prevent this, only god knows why...

They could have used a self detaching lifeCycle observer, which ability was already foresaw in its code, but nowhere in the android architecture makes use of that ability....

They should have used this unfinished component to listen to the global Fragments lifecycle instead of its viewLifeCycle, from here on, one can scale the listening from the Fragment to the viewLifeCycle. (attach/detach viewLifeCycle observer ON_START/ON_STOP)

Second... even if this is done, the fact that the viewPager itself is built on top of a recyclerView makes it extremely difficult to handle what you would expect from a Fragment's behavior, which is an state of preservation, a one time instantiation, and a well defined lifecycle (controllable/expected destruction).

This adapter is contradictory in its functionality, it checks if the viewPager has already been fed with Fragments, while still requiring a "fresh" adapter on reentrance.

It preserves Fragments on exit to the backStack, while expecting to recreate all of them on reentrance.

The breaks on place to prevent a field instantiated adapter, assuming all other variables are already accounted for a proper viewLifeCycle handling (registering/unregistering & setting and resetting of parameters) are:

@Override
    public final void restoreState(@NonNull Parcelable savedState) {
        if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
            throw new IllegalStateException(
                    "Expected the adapter to be 'fresh' while restoring state.");
        }
.....
}

Second break:

@CallSuper
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
    checkArgument(mFragmentMaxLifecycleEnforcer == null);
    mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
    mFragmentMaxLifecycleEnforcer.register(recyclerView);
}

where mFragmentMaxLifecycleEnforcer must be == null on reentrance or it throws an exception in the checkArgument().

Third: A Fragment garbage collector put in place upon reentrance (to the view, from the backstack) that is postDelayed at 10 seconds that attempts to Destroy off screen Fragments, causing memory leaks on all offscreen pages because it kills their respective FragmentManagers that controls their LifeCycle.

private void scheduleGracePeriodEnd() {
    final Handler handler = new Handler(Looper.getMainLooper());
    final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            mIsInGracePeriod = false;
            gcFragments(); // good opportunity to GC
        }
    };

    mLifecycle.addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                handler.removeCallbacks(runnable);
                source.getLifecycle().removeObserver(this);
            }
        }
    });

    handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS);
}

And all of them because of its main culprit: the constructor:

public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
        @NonNull Lifecycle lifecycle) {
    mFragmentManager = fragmentManager;
    mLifecycle = lifecycle;
    super.setHasStableIds(true);
}

Upvotes: 5

Yirujiwang
Yirujiwang

Reputation: 213

it can be fixed by viewPager2.isSaveEnabled = false

Upvotes: 21

Cozzamara
Cozzamara

Reputation: 1328

I've been struggling with this and none of the previous answers helped.

This may not work for every possible situation, but in my case fragments containing ViewPager2 were fixed and few, and I solved this by doing fragment switch with FragmentTransaction's show() and hide() methods, instead of replace() commonly recommended for this. Apply show() to the active fragment, and hide() to all others. This avoids operations like re-creating views, and restoring state that trigger the problem.

Upvotes: 0

I solved this problem by testing if it is equal null

if(recyclerView.adapter == null) {recyclerView.adapter = myAdapter}

Upvotes: 0

Boycott A.I.
Boycott A.I.

Reputation: 18871

I was also getting this java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state. when using ViewPager2 within a Fragment.

It seems the problem was because I was executing mViewPager2.setAdapter(mFragmentStateAdapter); in my onCreateView() method.

I fixed it by moving mViewPager2.setAdapter(mMyFragmentStateAdapter); to my onResume() method.

Upvotes: 1

Payam Roozbahani
Payam Roozbahani

Reputation: 1345

I encountered the same problem with ViewPager2. After a lot of efforts on testing different methods this worked for me:

public void onExitOfYourFragment() {
    viewPager2.setAdapter(null);
}

When you come back to the fragment again:

public void onResumeOfYourFragment() {
    viewPager2.setAdapter(yourAdapter);
}

Upvotes: 8

SMGhost
SMGhost

Reputation: 4037

So my problem was that I was creating my FragmentStateAdapter inside my Fragment class field where it was only created once. So when my onCreateView got called a second time I got this issue. If I recreate adapter on every onCreateView call, it seems to work.

Upvotes: 7

Related Questions