Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does FragmentStatePagerAdapter save fragment state on orientation change?

I have 3 fragments that need to be in a ViewPager. These fragment will hold dynamic information retrieved from a database. I understand that on an orientation change, the activity and fragments are destroyed and recreated. But I was under the impression by its name, that the FragmentStatePagerAdapter will save the state of the fragment. Apparently, I was wrong because every time I did something to the fragment, then change orientation, the fragment is reverted back to how it was laid out in the layout xml file.

As I was debugging, I noticed that on orientation change, the Adapter's getItem() method was never invoked - meaning that it wasn't recreated. So then how come the fragment state reverted back to its original state?

How do I save the fragment state using the FragmentStatePagerAdapter?

Please note that I have been following this tutorial and used their version of the SmartFragmentStatePagerAdapter.java class to manage the fragment dynamically.

And the following are my sample codes.

PageLoader.java - This interface allows MainActivity to manage the loading of the fragment pages dynamically at run time.

public interface PageLoader {
    void loadPage(int from, int target);
}

MainActivity.java

public class MainActivity extends AppCompatActivity implements PageLoader {
    MyPagerAdapter adapter;
    DirectionalViewPager pager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Get the ViewPager and set it's PagerAdapter so that it can display items
        pager = (DirectionalViewPager) findViewById(R.id.vpPager);
        adapter = new MyPagerAdapter(getSupportFragmentManager());
        pager.setOffscreenPageLimit(5);
        pager.setAdapter(adapter);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int position = pager.getCurrentItem();
        if (position > 0) pager.setCurrentItem(position - 1);
        return true;
    }

    @Override
    public void loadPage(int from, int target) {
        PageLoader fragment = (PageLoader) adapter.getRegisteredFragment(target);
        fragment.loadPage(from, target);
    }
}

MyPagerAdapter.java

public class MyPagerAdapter extends SmartFragmentStatePagerAdapter {
    private static final int NUM_ITEMS = 4;

    public MyPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    // Returns total number of pages
    @Override
    public int getCount() {
        return NUM_ITEMS;
    }

    // Returns the fragment to display for that page
    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0: // Fragment # 0 - This will show Frag1
                return Frag1.newInstance(position, Frag1.class.getSimpleName());
            case 1: // Fragment # 0 - This will show Frag1 different title
                return Frag1.newInstance(position, Frag1.class.getSimpleName());
            case 2: // Fragment # 1 - This will show Frag2
                return Frag2.newInstance(position, Frag2.class.getSimpleName());
            default:
                return Frag3.newInstance(position, Frag3.class.getSimpleName());
        }
    }

    // Returns the page title for the top indicator
    @Override
    public CharSequence getPageTitle(int position) {
        return "Page " + position;

    }
}

Frag1.java Frag2.java Frag3.java - these are all the same, except for the numbering.

public class Frag1 extends Fragment implements PageLoader {
    // Store instance variables
    private String title;
    private int page;
    private TextView txtView;

    // newInstance constructor for creating fragment with arguments
    public static Frag1 newInstance(int page, String title) {
        Frag1 fragmentFirst = new Frag1();
        Bundle args = new Bundle();
        args.putInt("someInt", page);
        args.putString("someTitle", title);
        fragmentFirst.setArguments(args);
        return fragmentFirst;
    }

    // Store instance variables based on arguments passed
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        page = getArguments().getInt("someInt", 0);
        title = getArguments().getString("someTitle");
    }

    // Inflate the view for the fragment based on layout XML
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.frag1, container, false);
        txtView = (TextView) view.findViewById(R.id.txt_frag1);
        txtView.setText(page + " - " + title);

        Button btn = (Button) view.findViewById(R.id.btn_frag1);
        btn.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PageLoader activity = (PageLoader) getActivity();
                activity.loadPage(page, page+1);

            }
        });
        return view;
    }

    @Override
    public void loadPage(int from, int target) {
        txtView.setText(txtView.getText() + "\nThis message was created from" + from + " to " + target);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.someone.smartfragmentstatepageradapter.custom.DirectionalViewPager
        android:id="@+id/vpPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </com.example.someone.smartfragmentstatepageradapter.custom.DirectionalViewPager>
</LinearLayout>

frag1.xml frag2.xml frag3.xml - again these are all the same except for the numbering

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#cc2">

    <TextView
        android:id="@+id/txt_frag1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="txt_frag1"
        />
    <Button
        android:id="@+id/btn_frag1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="btn_frag1"
        android:textSize="26dp" />

</LinearLayout>

PLEASE tell me how I can use the FragmentStatePagerAdapter to save the "State" of my fragments. I've been scouring the internet from 9am to 9pm today... 12 hours... I really need some help figuring this out. Thanks in advance!

like image 483
chaser Avatar asked Oct 19 '22 07:10

chaser


1 Answers

EDIT Try this:

Add another instance variable to your fragment:

    private String text;   // this is part of saved state

Set this variable in loadPage:

    @Override
    public void loadPage(int from, int target) {
        text = txtView.getText().toString() + "\nThis message was created from" + from + " to " + target;
        txtView.setText(text);
    }

Override onSaveInstanceState to save this variable:

    @Override
    public void onSaveInstanceState(Bundle outState);

        outState.putString("text", text);
        super.onSaveInstanceState(outState);
    }

Then restore the the TextView state using this variable:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {

        if (savedInstanceState != null) {
            // not null means we are restoring the fragment
            text = savedInstanceState.getString("text");
        } else {
            text = "" + page + " - " + title;
        }
        View view = inflater.inflate(R.layout.frag1, container, false);
        txtView = (TextView) view.findViewById(R.id.txt_frag1);
        txtView.setText(text);

        Button btn = (Button) view.findViewById(R.id.btn_frag1);
        btn.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PageLoader activity = (PageLoader) getActivity();
                activity.loadPage(page, page+1);

            }
        });
        return view;
    }

Any time you want something in your fragment to stay the same when things like configuration changes occur, this is how you would track the state, then save and restore it.


This is where you would use onSaveInstanceState for fragments and activities.

This is the method you would override to save any necessary state. Anything you change in your fragment that you want to have recreated on configuration change must be saved and then restored during onCreate or onCreateView.

So if you're trying to restore the text created in loadPage, you would create a class-level String for the text, set it in loadPage, save that in the onSaveInstanceState override, and then restore in in onCreateView from the savedInstanceState parameter.

Now here's the kicker: You are noticing that getItem on your adapter isn't called after a config change. But did you notice that your fragment is still there (even though it wasn't how you left it)? Keep in mind that the activity has a FragmentManager that is managing the fragments and their transactions. When the activity goes to config change, it saves its state. The FragmentManager and all of the active fragments are part of that state. Then the fragments are restored in such a way that adapter.getItem isn't called.

Turns out, that SmartFragmentPagerAdapter isn't so smart. It can't recreate its registeredFragments array after a configuration change, so it's really not very useful. I would discourage you from using it.

So how do you send events to off-page fragments when the ViewPager has appropriated the fragment's tag for its own use?

The technique I use is to define event listener interfaces, and have the fragments register as listeners with the activity. When I fire an event, it's by calling a method on the activity that notifies its active listeners. I give a pretty complete example of this in this answer.

like image 54
kris larson Avatar answered Nov 15 '22 05:11

kris larson