Android ViewPager, FragmentPagerAdapter and orientation changes

One thing I love doing is solving problems. Give me a problem I can’t solve and I will most likely lose my sleep over it.

Given this attitude, I have solved multiple problems over the decade, but recently I realized that I have no idea how many different kinds of problems I have solved. Well, I can recall some of them but not all of them. I decided that every time I solved an interesting problem I will write a blog about it. This is the first post of that series. I encountered an interesting problem today, and here I am forcing myself to blog.

I have a ViewPager and a PagerAdapter and a ViewPager.OnPageChangeListener. The code for OnPageChangeListener is as follows.

mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener()
{
@Override
public void onPageSelected(int position)
{
triggerVisible(position);
}
}

Since there are multiple Fragments in the ViewPager, and more than one Fragment gets created at one point of time, the Fragments do not themselves know when they get visible. `triggerVisible` is the function that notifies the Fragment that they are now visible.

private void triggerVisible(final int position)
{
mPager.post(new Runnable()
{
@Override
public void run()
{
Fragment frag = mPagerAdapter.getItem(position);
if(frag instanceof OnFragmentVisibleCallback)
{
((OnFragmentVisibleCallback) frag).onVisible();
}
}
});
}

This worked like a charm. Every time a user would swipe on the view pager, the fragment would get notified that they are visible.

The problem : While viewing one of the Fragments, if we rotate the device, the `onVisible()` function on the Fragment is called, but the Fragment does not change its UI as if the `onVisible()` function was never called.

Now here is the reason why this was happening. If you are the kind who likes not to read answers and give a shot to answering the question, don’t scroll below until you have thought of the reason why this could be happening.


There is only one way this could be happening. The Fragment on which I am calling the `onVisible()` is different than the Fragment that is visible. I quickly verified this by logging the `hashcode()` of the fragments and I was right, both were different fragment. Now the question was, Why?


I don’t claim to have solved this on my own. I found a good description at this stackoverflow link: http://stackoverflow.com/questions/7951730/viewpager-and-fragments-whats-the-right-way-to-store-fragments-state

The FragmentPagerAdapter adds the Fragments to FragmentManager by using a special tag which defines the position of the Fragment in the pager. The method `FragmentPagerAdapter.getItem(int position)` is only called when a fragment at the given position does not exist. This totally makes sense.

Now when the device’s orientation changes the Fragments are not actually destroyed, so they are reused.

Now in order to call `onVisible()` function on the Fragment I was using mPagerAdapter.getItem(position); which would return me a new instance of the Fragment, hence the onVisible() function call was no longer working. I don’t believe that was something wrong with the getItem(position) function.

@Override
public Fragment getItem(int position)
{
Fragment frag = mFragments[position];
if (frag == null)
{
frag = ArticleDetailFragment.newInstance(
mCursor.getLong(ArticleCursorLoader.Query._ID));
mFragments[position] = frag;
}
return frag;
}

Damn, I was like why Isn’t ViewPager using the getItem(position) function.


That was because of the method instantiateItem(ViewGroup container, int position) function. I looked into the code, and I believe this is how the framework is working.

In the normal scenario when there is no orientation change, first the method PagerAdapter.instantiateItem(ViewGroup container, int position) is called. If the fragment for the position does not exists then getItem(position) is called and the returned Fragment is added to the Fragment list.

When there is an orientation change, instantiateItem notices that there is already a fragment for the given position, so it reused that fragment instead of getting the Fragment from getItem(position).

After I understood this, I made small change to the code. and viola!, It worked.

public Object instantiateItem(ViewGroup container, int position)
{
Object ret = super.instantiateItem(container, position);
mFragments[position] = (Fragment) ret;
return ret;
}

Now both my getItem(position) and instantiateItem return the same fragment.


See you until next Time.