Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I maintain fragment state when added to the back stack?

If you return to a fragment from the back stack it does not re-create the fragment but re-uses the same instance and starts with onCreateView() in the fragment lifecycle, see Fragment lifecycle.

So if you want to store state you should use instance variables and not rely on onSaveInstanceState().


Comparing to Apple's UINavigationController and UIViewController, Google does not do well in Android software architecture. And Android's document about Fragment does not help much.

When you enter FragmentB from FragmentA, the existing FragmentA instance is not destroyed. When you press Back in FragmentB and return to FragmentA, we don't create a new FragmentA instance. The existing FragmentA instance's onCreateView() will be called.

The key thing is we should not inflate view again in FragmentA's onCreateView(), because we are using the existing FragmentA's instance. We need to save and reuse the rootView.

The following code works well. It does not only keep fragment state, but also reduces the RAM and CPU load (because we only inflate layout if necessary). I can't believe Google's sample code and document never mention it but always inflate layout.

Version 1(Don't use version 1. Use version 2)

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // (it will be added back).
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        return _rootView;
    }
}

------Update on May 3 2005:-------

As the comments mentioned, sometimes _rootView.getParent() is null in onCreateView, which causes the crash. Version 2 removes _rootView in onDestroyView(), as dell116 suggested. Tested on Android 4.0.3, 4.4.4, 5.1.0.

Version 2

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // in onDestroyView() (it will be added back).
        }
        return _rootView;
    }

    @Override
    public void onDestroyView() {
        if (_rootView.getParent() != null) {
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        super.onDestroyView();
    }
}

WARNING!!!

This is a HACK! Though I am using it in my app, you need to test and read comments carefully.


I guess there is an alternative way to achieve what you are looking for. I don't say its a complete solution but it served the purpose in my case.

What I did is instead of replacing the fragment I just added target fragment. So basically you will be going to use add() method instead replace().

What else I did. I hide my current fragment and also add it to backstack.

Hence it overlaps new fragment over the current fragment without destroying its view.(check that its onDestroyView() method is not being called. Plus adding it to backstate gives me the advantage of resuming the fragment.

Here is the code :

Fragment fragment=new DestinationFragment();
FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction ft=fragmentManager.beginTransaction();
ft.add(R.id.content_frame, fragment);
ft.hide(SourceFragment.this);
ft.addToBackStack(SourceFragment.class.getName());
ft.commit();

AFAIK System only calls onCreateView() if the view is destroyed or not created. But here we have saved the view by not removing it from memory. So it will not create a new view.

And when you get back from Destination Fragment it will pop the last FragmentTransaction removing top fragment which will make the topmost(SourceFragment's) view to appear over the screen.

COMMENT: As I said it is not a complete solution as it doesn't remove the view of Source fragment and hence occupying more memory than usual. But still, serve the purpose. Also, we are using a totally different mechanism of hiding view instead of replacing it which is non traditional.

So it's not really for how you maintain the state, but for how you maintain the view.


I would suggest a very simple solution.

Take the View reference variable and set view in OnCreateView. Check if view already exists in this variable, then return same view.

   private View fragmentView;

   public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        if (fragmentView != null) {
            return fragmentView;
        }
        View view = inflater.inflate(R.layout.yourfragment, container, false);
        fragmentView = view;
        return view;
    }

I came across this problem in a Fragment containing a map, which has too many setup details to save/reload. My solution was to basically keep this Fragment active the whole time (similar to what @kaushal mentioned).

Say you have current Fragment A and wants to display Fragment B. Summarizing the consequences:

  • replace() - remove Fragment A and replace it with Fragment B. Fragment A will be recreated once brought to the front again
  • add() - (create and) add a Fragment B and it overlap Fragment A, which is still active in the background
  • remove() - can be used to remove Fragment B and return to A. Fragment B will be recreated when called later on

Hence, if you want to keep both Fragments "saved", just toggle them using hide()/show().

Pros: easy and simple method to keep multiple Fragments running
Cons: you use a lot more memory to keep all of them running. May run into problems, e.g. displaying many large bitmaps


onSaveInstanceState() is only called if there is configuration change.

Since changing from one fragment to another there is no configuration change so no call to onSaveInstanceState() is there. What state is not being save? Can you specify?

If you enter some text in EditText it will be saved automatically. Any UI item without any ID is the item whose view state shall not be saved.