5

I know this question has been asked before but none of the answers given so far is of any help to me.

I have a viewpager which is populated with fragments (android.support.v4.app.Fragment) from a FragmentStatePagerAdapter . Some of these fragments contain logic that needs to be retained when the orientation changes, such as keeping track of which view is currently selected.

However, although I save the data in question in onSaveInstanceState the savedInstanceState is always null. I can solve this by storing the data in a static variable (which since I only have one instance of each fragment would work for me) but i found this to be a quite ugly solution and there has to be a proper way of doing this.

This is one of the fragments that doesn't retain it's state on rotation:

    public class PriceSelectFragment extends Fragment {

    private TableRow mSelected;
    private int mSelectedPos = 0;

    // newInstance constructor for creating fragment with arguments
    public static PriceSelectFragment newInstance() {
        PriceSelectFragment fragmentFirst = new PriceSelectFragment();
        return fragmentFirst;
    }

    public PriceSelectFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_price_select, container, false);
        TableLayout mTable = (TableLayout)view.findViewById(R.id.price_table);

        List<PriceGroup> mPriceGroups = ((MainActivity) getActivity()).getPriceGroups();

        int i = 0;
        for (final PriceGroup group : mPriceGroups) {
            //Create row from layout and access child TextViews
            TableRow r = (TableRow)inflater.inflate( R.layout.price_group, mTable, false);
            TextView size = (TextView)r.getChildAt(0);
            TextView dimension = (TextView)r.getChildAt(1);
            TextView weight = (TextView)r.getChildAt(2);
            TextView price = (TextView)r.getChildAt(3);

            //Populate row with PriceGroup Data
            size.setText(group.sizeIndicator);
            dimension.setText(String.format("%2.0fx%2.0fx%2.0f", group.length, group.width, group.height));
            weight.setText(Float.toString(group.weight));
            price.setText(Integer.toString(group.price));

            //Alternate background color every other row
            if (i % 2 == 0) {
                r.setBackgroundDrawable(getResources().getDrawable(R.drawable.price_selector_1));
            }
            else {
                r.setBackgroundDrawable(getResources().getDrawable(R.drawable.price_selector_2));
            }
            mTable.addView(r); // Add to table

            r.setTag(i);
            r.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    selectRow((TableRow) v);
                }
            });
            i++;
        }

        mSelected = (TableRow)view.findViewWithTag(mSelectedPos);
        selectRow(mSelected);

        return view;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("selected", mSelectedPos);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (savedInstanceState != null) {
            mSelectedPos = savedInstanceState.getInt("selected");
        }
    }

    private void selectRow(TableRow row) {
        if ((int) mSelected.getTag() % 2 == 0) {
            mSelected.setBackgroundDrawable(getResources().getDrawable(R.drawable.price_selector_1));
        }
        else {
            mSelected.setBackgroundDrawable(getResources().getDrawable(R.drawable.price_selector_2));
        }
        mSelected = row;
        mSelectedPos = (int) mSelected.getTag();
        mSelected.setBackgroundColor(getResources().getColor(R.color.light_blue));
    }


}

How do I solve this without having to save my states in static variables?

Edit

I should point out that all of the fragments are programatically created and as such they do not have an id and I read that that might be the problem but I don't know how to solve that either.

Also my application is structured like this:

  • MainActivity with NavigationDrawer
    • Fragment1
      • ViewPager
        • subfragment1 - subfragment5
    • Fragment2
    • Fragment3

The fragments whose states I'm having trouble with are the subfragments.

Nobbe
  • 279
  • 4
  • 11
  • Please add how you're using the fragments in your activity – Zharf Apr 29 '15 at 13:33
  • The viewpager is located in another fragment in a navigationdrawer, should I add the code for that fragment or the MainActivity? – Nobbe Apr 29 '15 at 13:36
  • Are you saving the fragments state in the activitys `onSaveInstanceState` method? – Dreagen Apr 29 '15 at 13:40
  • Are you telling me I have to send the "state" data from all my five fragments to my main activity and save them there? And then get them back from my activity to the fragments? I considered doing that but I hoped there would be a more elegant solution. Is there no way to handle the states "locally" in the fragments themselves? Or at least in the parent fragment? Or the ViewPager? – Nobbe Apr 29 '15 at 13:44
  • It shouldn't be quite that complicated. You have to remember that when you rotate your device the activity which contains your fragment gets recreated. I'll post an answer with a bit more detail – Dreagen Apr 29 '15 at 13:46

3 Answers3

4

In your Activity which is hosting your Fragment you need to store a refernce to the fragment in the Bundle.

Something like this should work for you

public void onCreate(Bundle savedInstanceState) {

    if (savedInstanceState != null) {
        //Restore your fragment instance
        fragment1 = getSupportFragmentManager().getFragment(
                    savedInstanceState, "fragment");
    }
}


protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    getSupportFragmentManager().putFragment(outState, "fragment", fragment1);
}

fragment1 is the instance of the Fragment1 that you mentioned in your question that needs to get recreated.

I haven't done this with a structure like yours before but this is how I would start:

In the onSaveInstanceState in your Fragment1 I believe you would need to do the same with each of the fragments in your ViewPager. Then in the onCreateView on your Fragment1 get the fragments from the fragment manager and recreate your ViewPager.

I have found this answer here which is pretty much the same but has a little more detail: https://stackoverflow.com/a/17135346/1417483

Community
  • 1
  • 1
Dreagen
  • 1,733
  • 16
  • 21
  • Your structure looks quite complex, but I think if you store all the fragments you need to recreate in the `onSaveInstanceState` method and get them back out in `onCreate` it should still work – Dreagen Apr 29 '15 at 14:02
  • Ok, I updated my question so you can see the structure of my app. I'm supposed to put this in my MainActivity to store Fragment1 correct? But how does that affect the subfragments in the ViewPager? Do I not have to save their states separately? Or are their states saved along with Fragment1? – Nobbe Apr 29 '15 at 14:03
  • I have never actually done it on a layout like yours so I'm 100% sure. But I would imagine your 'inner' fragments would get stored along with Fragment1 and as long as you stored anything from each of those inner fragments in their own `onSaveInstanceState` method it would all re create as you would expect – Dreagen Apr 29 '15 at 14:05
  • I'll try to be a bit clearer. I think you only need to store Fragment 1 in your activity's `onSaveInstancestate`, but as I said. Have a play with it and see – Dreagen Apr 29 '15 at 14:06
  • Alright, that makes sense, I'll try it out tonight and hopefully it works. Thanks! – Nobbe Apr 29 '15 at 14:11
  • I thought a bit more about it and I have updated my answer with how I think it needs to be done. As I said I haven't done exactly this myself but hopefully it'll work and if not it should point you in the right direction – Dreagen Apr 29 '15 at 14:13
  • 1
    Ok, I took me a while to get around to try this, but it works. Thanks again for the help! – Nobbe May 07 '15 at 09:18
  • I think this answer doesn't really help, as the ViewPager together with its PagerAdapter (FragmentStatePagerAdapter) should and does handle the adding and removal from the fragmentManager. You are not supposed to do getFragmentManager().putFragment, as this happenes inside the PagerAdapter. – FrankKrumnow Apr 20 '16 at 10:28
1

FragmentPagerAdapter is not calling onSaveInstanceState in frgments that are not visible anymore. Maybe this is what causing your issues. Try to use FragmentStatePagerAdapter instead.

EdgarK
  • 870
  • 7
  • 10
0

I finally got a solution and explanation why this is happening. I had a very similar problem. I recognized that when I was scrolling right to my 3rd subfragment and then back to the 1st then the state of the 1st got saved. But not on Orientation Change.

I figured that the state is only saved if the adapter's destroyItem(..) is called. That is not called automatically if orientation changes.

So now onSaveInstanceState of the MainFragment (which holds the ViewPager) I call destroyItem for each active fragment. I check for activity.isChangingConfigurations() because onSaveInstanceState is called too if I turn off the screen, but in that case all the fragments just stay active and nothing has to be changed.

I extended the adapter with an onDestroy(boolean retain) which is called then:

//in the main-fragment which holds the ViewPager:
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if(getActivity()!=null && getActivity().isChangingConfigurations())
    {
        if (pager != null) {
        try {
            Log.w(TAG, TAG + " pager.onDestroy(true)");
            pager.onDestroy(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

And the implementation in MyFragmentStatePagerAdapter:

public void onDestroy(boolean retain)
{
    this.m_allowDynamicLoading = false;
    if(retain)
    {
        try{
            if(getAdapter()!=null)
            {
                int limit = this.getOffscreenPageLimit();
                int currentIndex = this.getCurrentItem();
                if(currentIndex <0 || getAdapter().getCount() <= 0) 
                    return;
                //active fragments = fragments that are (less or equal) then
                //offscreenPageLimit awaw from the currently displayed one.
                for(int i = Math.min(currentIndex+limit, getAdapter().getCount()-1); 
                        i>= Math.max(0, currentIndex-limit);//erstes aktives fragment ist current - offscreen limit, aber nicht unter 0..
                        i--)    
                {
                    getAdapter().destroyItem(MessagingViewPager.this, i, getAdapter().instantiateItem(MessagingViewPager.this, i)); //this saved the state of that fragment, that will be restored after orientation change
                    Log.e(TAG,TAG + " orientation-change: destroying item " + i);                   
                }
            }
        }catch(Exception e){}   
    }
    else{ //retain = false is called onDestroy of the Fragment holding this Pager.
        try{
            this.setAdapter(null); 
            //this will destroy all fragments and forget the position
        }catch(Exception e){}   
    }       
}

Some other things are to be said:

  1. Adapter takes the ChildFragmentManager not the normal one
  2. The SubFragments must NOT use setRetainInstance(true) (Exception otherwise) The MainFragment can (and in my case does) use setRetainInstance(true)
  3. Create the adapter in onCreate of the MainFragment, so it will NOT be recreated on Orientation change. Setting adapter to pager should be done in onCreateView.
  4. OnDestroy (or onDestroyView) of the MainFragment use setAdapter(null) to terminate all fragments and release resources. (This is done by MyViewPager.onDestroy(false) in my case)

et voiá: now you get your savedInstanceState bundle in the SubFragments after the orientation change. And it will not destroy the items if you only switch the screen off.

FrankKrumnow
  • 501
  • 5
  • 13