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

后端 未结 6 1274
小鲜肉
小鲜肉 2020-11-21 11:57

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

相关标签:
6条回答
  • 2020-11-21 12:02

    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.

    0 讨论(0)
  • 2020-11-21 12:08
    final FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.hide(currentFragment);
    ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
    ft.addToBackStack(null);
    ft.commit();
    
    0 讨论(0)
  • 2020-11-21 12:09

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

    Hope this helps.

    0 讨论(0)
  • 2020-11-21 12:12

    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);
        }
        ...
    }
    
    0 讨论(0)
  • 2020-11-21 12:22

    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.

    0 讨论(0)
  • 2020-11-21 12:23

    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
    }
    
    0 讨论(0)
提交回复
热议问题