Nobbe
Nobbe

Reputation: 289

Fragment in viewpager savedinstancestate is always null

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:

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

Upvotes: 5

Views: 5916

Answers (3)

EdgarK
EdgarK

Reputation: 890

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

Upvotes: 1

FrankKrumnow
FrankKrumnow

Reputation: 508

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.

Upvotes: 0

Dreagen
Dreagen

Reputation: 1743

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

Upvotes: 4

Related Questions