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

7

543

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!

Goodin answered 9/3, 2013 at 17:15 Comment(0)
F
585

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

}
Feder answered 16/6, 2013 at 16:24 Comment(21)
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 :)Primus
@wizurd mContent is a Fragment, it's reference to the instance of the current fragment in the activity.Feder
if you have fragments that may or may not be present (created, destroyed, or replaced by user actions) you can in Activity's onCreate, if(savedInstanceState != null) do this: if(savedInstanceState.containsKey(FRAG_TAG))){ frag1 = (FragOne)getFragmentManager().getFragment(savedInstanceState, FRAG_TAG);Archive
Probably have to cast return from getFragment() ie, mContent = (ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState, "mContent");Archive
After orientation change it is not storing fragments which is in back stackHeigho
Can you explain how this will save the instance state of a fragment in back stack? That's what the OP asked.Mashburn
Can somebody explain how is this answer related to saving state of fragment which is in the backstack?Acaroid
This is not related to question, onSaveInstance is not called when fragment is putted to backstackCrescin
Why we need to do work in activity's onSaveInstanceState and onCreate? Will call to super in fragment will not do it automatically.Amass
@Feder after restoring fragment instance in mContent, where is mContent used further?Roanne
Shouldn't you call super.onSaveInstanceState(outState); after you update the outstate with your arguments to store?Inhambane
This is not required if you create the fragment correctly. The fragment should be created when the savedInstanceState is null. otherwise, you will restore the fragment, but it will be replaced with a brand new fragmentBruiser
Thank you a thousand times for mentioning putFragment and getFragment. Those methods are not famous enough yet!Abc
I think the fragment's vars should be restored in onCreate and not in onActivityCreated. In fact there are some cases when onActivityCreated is not called (i.e. when fragment is restored but in BackStack) but you need your vars correctly restored.Anatollo
Does not setRetainInstance(true) save fragments instance state? Is there a need to call onSaveInstanceState for fragments?Daciadacie
Is there a reason to use Activity's onCreate() rather than onRestoreInstanceState()?Buckbuckaroo
I don't think you need #2 for saving a basic piece of data in a Fragment. Refer to link inthecheesefactory.com/blog/…Indisposition
When I do this, I get java.lang.RuntimeException: Unable to start activity ComponentInfo{my.app/my.app.MyActivity}: java.lang.IllegalStateException: Fragment no longer exists for key myFragmentName: index 0. How do I solve this?Godding
Why onActivityCreated and not onCreate?Sarad
This does not make sense. The onCreate of the activity is not called if the app is still in the background.Knapp
What is mMyFragment?Jumbo
G
92

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.

Goodin answered 9/3, 2013 at 18:43 Comment(8)
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().Rillings
@Rillings I wish I could give you 5 up votes for this comment! This rotation twice thing has been killing me for days.Ermaermanno
Thank you for the great answer! I have one question though. Where is the best place to instantiate model object (POJO) in this fragment?Glasses
@Rillings your comment saved my day..It is more helpful than all the answers posted for the question..Thanks a lot!!Cunctation
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")Poultryman
savedInstanceState is null i don't know why !!Duwe
The main thing I didn't fully understand here is why you put one Bundle (savedState) inside another Bundle (savedInstanceState)? Is this because we do not know for sure, which is called first: onDestroyView() or onSaveInstanceState()?Borries
Don't work. View v = inflater.inflate(R.layout.fragment, null) is wrong. I can't use this. In my case it's like View v = inflater.inflate(R.layout.fragment, container, false) I can't use null.Sympathize
B
67

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.

Bog answered 6/10, 2014 at 21:32 Comment(8)
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!Demoss
@Demoss 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.Bog
@Bog does this apply if using a ViewPager?Preconize
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.Bog
Could you specify which version mentioned above?Blanco
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.Retreat
I agreed that this contribution should be the correct answer. But i need to add, that for this to work, you need to no override the onSaveInstanceState method of your activity or to call super.onSaveInstanceState in your implementation.Jackdaw
What is newInstance()? This doesn't work.Sympathize
E
17

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);
    }
    ...
}
Ermaermanno answered 2/11, 2013 at 16:56 Comment(2)
Can you tell me...What is "MyObject " here?Heigho
Anything you want it to be. It is just an example representing something that would be saved in the bundle.Ermaermanno
S
5

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
}
Susquehanna answered 27/8, 2015 at 22:26 Comment(0)
K
0

Using this :

  private var mData: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState != null) {
            mData= savedInstanceState.getString("Data")
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("mData", Data)
    }
Kettledrummer answered 11/2, 2023 at 14:38 Comment(0)
C
-9
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();
Community answered 21/12, 2016 at 6:45 Comment(1)
Not related to the original question.Danner

© 2022 - 2024 — McMap. All rights reserved.