ChatGPT解决这个技术问题 Extra ChatGPT

How to correctly save instance state of Fragments in back stack?

I have found many instances of a similar question on SO but no answer unfortunately meets my requirements.

I have different layouts for portrait and landscape and I am using back stack, which both prevents me from using setRetainState() and tricks using configuration change routines.

I show certain information to the user in TextViews, which do not get saved in the default handler. When writing my application solely using Activities, the following worked well:

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}

With Fragments, this works only in very specific situations. Specifically, what breaks horribly is replacing a fragment, putting it in the back stack and then rotating the screen while the new fragment is shown. From what I understood, the old fragment does not receive a call to onSaveInstanceState() when being replaced but stays somehow linked to the Activity and this method is called later when its View does not exist anymore, so looking for any of my TextViews results into a NullPointerException.

Also, I found that keeping the reference to my TextViews is not a good idea with Fragments, even if it was OK with Activity's. In that case, onSaveInstanceState() actually saves the state but the problem reappears if I rotate the screen twice when the fragment is hidden, as its onCreateView() does not get called in the new instance.

I thought of saving the state in onDestroyView() into some Bundle-type class member element (it's actually more data, not just one TextView) and saving that in onSaveInstanceState() but there are other drawbacks. Primarily, if the fragment is currently shown, the order of calling the two functions is reversed, so I'd need to account for two different situations. There must be a cleaner and correct solution!

I second the emunee.com link. It solved a stick UI problem for me!

A
Anye

To correctly save the instance state of Fragment you should do the following:

1. In the fragment, save instance state by overriding onSaveInstanceState() and restore in onActivityCreated():

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        
        //Save the fragment's state here
    }

}

2. And important point, in the activity, you have to save the fragment's instance in onSaveInstanceState() and restore in onCreate().

class MyActivity extends Activity {

    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
            
        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

This worked perfectly for me! No workarounds, no hacks, just makes sense this way. Thank you for this, made hours of searching successful. SaveInstanceState() of your values in fragment, then save fragment in Activity holding the fragment, then restore :)
@wizurd mContent is a Fragment, it's reference to the instance of the current fragment in the activity.
Probably have to cast return from getFragment() ie, mContent = (ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState, "mContent");
Can you explain how this will save the instance state of a fragment in back stack? That's what the OP asked.
This is not related to question, onSaveInstance is not called when fragment is putted to backstack
T
The Vee

This is a very old answer.

I don't write for Android anymore so function in recent versions is not guaranteed and there won't be any updates to it.

This is the way I am using at this moment... it's very complicated but at least it handles all the possible situations. In case anyone is interested.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Alternatively, it is always a possibility to keep the data displayed in passive Views in variables and using the Views only for displaying them, keeping the two things in sync. I don't consider the last part very clean, though.


This is the best solution I've found so far but there is still one (somewhat exotic) problem remaining: if you have two fragments, A and B, where A is currently on the backstack and B is visible, then you lose the state of A (the invisible one) if you rotate the display twice. The problem is that onCreateView() does not get called in this scenario, only onCreate(). So later, in onSaveInstanceState() there are no views to save the state from. One would have to store and then save the state passed in onCreate().
@devconsole I wish I could give you 5 up votes for this comment! This rotation twice thing has been killing me for days.
Thank you for the great answer! I have one question though. Where is the best place to instantiate model object (POJO) in this fragment?
@devconsole your comment saved my day..It is more helpful than all the answers posted for the question..Thanks a lot!!
To help save time for others, App.VSTUP and App.STAV are both string tags that represent the objects they are trying to obtain. Example: savedState = savedInstanceState.getBundle(savedGamePlayString); or savedState.getDouble("averageTime")
R
Ricardo

On the latest support library none of the solutions discussed here are necessary anymore. You can play with your Activity's fragments as you like using the FragmentTransaction. Just make sure that your fragments can be identified either with an id or tag.

The fragments will be restored automatically as long as you don't try to recreate them on every call to onCreate(). Instead, you should check if savedInstanceState is not null and find the old references to the created fragments in this case.

Here is an example:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}

Note however that there is currently a bug when restoring the hidden state of a fragment. If you are hiding fragments in your activity, you will need to restore this state manually in this case.


Is this fix something you noticed while using the support library or did you read about it somewhere? Is there any more information you could provide about it? Thanks!
@Piovezan it can be sort of implicitly inferred from the docs. For example, the beginTransaction() doc reads as follow: "This is because the framework takes care of saving your current fragments in the state (...)" . I have also been coding my apps with this expected behavior for quite some time now.
@Ricardo does this apply if using a ViewPager?
Normally yes, unless you changed the default behavior on your implementation of FragmentPagerAdapter or FragmentStatePagerAdapter. If you look at the code of FragmentStatePagerAdapter, for example, you will see that the restoreState() method restores the fragments from the FragmentManager you passed as parameter when creating the adapter.
I think this contribution is the best answer to the original question. It's also the one that - in my opinion - is best aligned with how the Android platform works. I would recommend marking this answer as the "Accepted" one to better help future readers.
D
DroidT

I just want to give the solution that I came up with that handles all cases presented in this post that I derived from Vasek and devconsole. This solution also handles the special case when the phone is rotated more than once while fragments aren't visible.

Here is were I store the bundle for later use since onCreate and onSaveInstanceState are the only calls that are made when the fragment isn't visible

MyObject myObject;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}

Since destroyView isn't called in the special rotation situation we can be certain that if it creates the state we should use it.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

This part would be the same.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Now here is the tricky part. In my onActivityCreated method I instantiate the "myObject" variable but the rotation happens onActivity and onCreateView don't get called. Therefor, myObject will be null in this situation when the orientation rotates more than once. I get around this by reusing the same bundle that was saved in onCreate as the out going bundle.

    @Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}

Now wherever you want to restore the state just use the savedState bundle

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}

Can you tell me...What is "MyObject " here?
Anything you want it to be. It is just an example representing something that would be saved in the bundle.
T
The Guy with The Hat

Thanks to DroidT, I made this:

I realize that if the Fragment does not execute onCreateView(), its view is not instantiated. So, if the fragment on back stack did not create its views, I save the last stored state, otherwise I build my own bundle with the data I want to save/restore.

1) Extend this class:

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}

2) In your Fragment, you must have this:

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restore your data here

    return true;
}

3) For example, you can call hasSavedState in onActivityCreated:

@Override
public void onActivityCreated(Bundle state) {
    super.onActivityCreated(state);

    if (hasSavedState()) {
        return;
    }

    //your code here
}

N
Nilesh Savaliya
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

Not related to the original question.