Displaying Fragments with data in a ViewPager on screen rotation - android

So in my MainActivity OnCreate I create 3 Fragments, each filled with different data. I then add these Fragments to my ViewPagerAdapter.
ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
MealsFragment exploreFragment = null;
MealsFragment favoriteFragment = null;
MealsFragment localFragment = null;
//if there is no saved instance, create fragments with passed data as args and add them to the viewpageradapter
if(savedInstanceState == null){
exploreFragment = fetchExploreMealsDataAndCreateFragment();
favoriteFragment = fetchFavoriteMealsDataAndCreateFragment();
localFragment = fetchLocalMealsDataAndCreateFragment();
}
//add fragments to the adapter
viewPagerAdapter.addFragment(exploreFragment, "EXPLORE");
viewPagerAdapter.addFragment(favoriteFragment, "FAVORITES");
viewPagerAdapter.addFragment(localFragment, "LOCAL");
//set adapter to the viewpager and link it with the different tabs
mviewPager.setAdapter(viewPagerAdapter);
mtabLayout.setupWithViewPager(mviewPager);
The 3 different fetch methods get the data from an API or DB, so I don't want to call these methods every time I rotate my screen and go through my lifecycle. That's why I firstly check if the savedInstanceState is null. But what happens now is that if my savedInstanceState is not null, the Fragments will be null since i initialised them that way.
Apparently this is not a problem since when I rotate the screen, the fragments remain the same. I was wondering what is going on here behind the scenes as I don't think this is the correct way of handling my situation. Any suggestions of improvement are appreciated aswell.
Thanks in advance!
EDIT:
Forgot to mention that ViewPagerAdapter is my own implementation of FragmentPageAdapter
public class ViewPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> fragmentList = new ArrayList<>();
private final List<String> fragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
return fragmentList.get(i);
}
#Override
public int getCount() {
return fragmentList.size();
}
#Nullable
#Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
public void addFragment(Fragment fragment, String title){
fragmentList.add(fragment);
fragmentTitleList.add(title);
}
}

I was wondering what is going on here behind the scenes
Alright, so, this is going to involve looking at the source for FragmentPagerAdapter. What's relevant is the implementation of the instantiateItem() method... though really just a portion of it.
#NonNull
#Override
public Object instantiateItem(#NonNull ViewGroup container, int position) {
[...]
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
[...]
}
Essentially, the getItem() method from your ViewPagerAdapter is only called once for each position for the lifetime of the Activity (even across configuration changes). Every time other than the first, the Fragment object is retrieved directly from the FragmentManager as opposed to the Adapter.
So yes, for all re-creations of your Activity, your Adapter is holding a List<Fragment> that is just null, null, null... but it doesn't matter, because this list is not accessed again.
HOWEVER
The above statements assume that every Fragment in your adapter was constructed and added to the FragmentManager before your configuration change, and this is not necessarily guaranteed.
By default, the off-screen page limit for the ViewPager is 1. That means that your third page (your localFragment), is not necessarily added to the ViewPager, and therefore the FragmentManager, on first launch. If you scroll over to the next page even one time, it will be, but this is not necessarily true.
Or perhaps you have manually set the off-screen page limit to be 2, in which case all the pages/Fragments will be added immediately.
Probably the best thing to do is to change how you're using FragmentPagerAdapter altogether. I'd put this inside your Activity as an inner class:
private class ExampleAdapter extends FragmentPagerAdapter {
public ExampleAdapter() {
super(getSupportFragmentManager());
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0: return fetchExploreMealsDataAndCreateFragment();
case 1: return fetchFavoriteMealsDataAndCreateFragment();
case 2: return fetchLocalMealsDataAndCreateFragment();
default: throw new IllegalArgumentException("unexpected position: " + position);
}
}
#Nullable
#Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0: return "EXPLORE";
case 1: return "FAVORITES";
case 2: return "LOCAL";
default: throw new IllegalArgumentException("unexpected position: " + position);
}
}
#Override
public int getCount() {
return 3;
}
}
And then your onCreate() could be changed to this:
FragmentPagerAdapter viewPagerAdapter = new ExampleAdapter();
mviewPager.setAdapter(viewPagerAdapter);
mtabLayout.setupWithViewPager(mviewPager);
The advantage of doing it this way is that you only call the "fetch and create" methods on demand, but still have the ability to call them after a configuration change in situations where they weren't loaded before the configuration change.

Related

Fragment update view in view pager

I am using view pager which is having 3 fragments
public class ViewPagerAdapter extends FragmentStatePagerAdapter {
private static final String TAG="ViewPageAdapter";
private final int PAGES = 3;
private String[] title = new String[]{"Frag1",
"Frag2", "Frag2"};
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
Log.d(TAG, "position is " + position);
switch (position) {
case 0:
return new Frag1();
case 1:
return new Frag2();
case 2:
return new Frag3();
default:
throw new IllegalArgumentException(
"The item position should be less or equal to:" + PAGES);
}
}
#Override
public int getCount() {
return PAGES;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
#Override
public CharSequence getPageTitle(int position) {
return title[position];
}
}
I am setting view pager from main activity:
viewPageAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.addOnPageChangeListener(onPageChangeListener);
viewPager.setAdapter(viewPageAdapter);
When i execute viewPageAdapter.notifyDataSetChanged();
or viewPager.setAdapter(viewPageAdapter); method to recreate fragments the activity is getting destroyed.don't know what causing the issue.
I checked many solution nothing worked.
ViewPager PagerAdapter not updating the View
Update ViewPager dynamically?
I am asking another problem because which is related to above question.
The problem which is described in link
ViewPager onPageSelected for first page
where i am not able to get first page tab title on load.i am using android.support.v4.view.PagerTabStrip
Please help me in getting solved. Wasted my whole day to get rid of this :(

How to add Fragment to first position in FragmentPagerAdapter

I am using FragmentPagerAdapter and a ViewPager to add custom Fragments EDIT: from my MainActivity (also sending a bunch of extra data based on a JSON response via bundle) and using swiping motions to move to the next Fragments in the List.
public class MyPagerAdapter extends FragmentPagerAdapter implements Serializable {
public List<Fragment> fragments;
public FragmentManager fm;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
this.fm = fm;
this.fragments = new ArrayList<Fragment>();
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
}
Everything is working fine as long as I'm adding new Fragments by
using
MyPagerAdapter pageAdapter = new MyPagerAdapter(getSupportFragmentManager());
pager = (ViewPager)findViewById(R.id.myViewPager);
pageAdapter.fragments.add(new CustomFragment());
pager.setAdapter(pageAdapter);
But I can't find a proper way to add Fragments to the beginning of the List and swipe back.
I've tried both
pageAdapter.fragments.add(0, new CustomFragment());
as well as changing the FragmentPagerAdapters List to LinkedList and using
pageAdapter.fragments.addFirst(new CustomFragment());
and then refreshing the adapter by using
pageAdapter.notifyDataSetChanged();
and i keep getting the following exception:
java.lang.IllegalStateException: Can't change tag of fragment CustomFragment{2ead9520 #10 id=0x7f0a0002 android:switcher:2131361794:10}: was android:switcher:2131361794:10 now android:switcher:2131361794:11
The key methods no one has talked about yet is public int getItemPosition(Object) which is used to remap fragments to pages after they move and public long getItemId(int position) which must be overridden by a pager adapter that reorders fragments. The default implementation uses the position of the page as the id, so reordering confuses the FragmentPagerAdapter.
(I am leaving out the Serializable interface as it is irrelevant for the purposes of answering the question - How to reorder fragments in a FragmentPagerAdapter).
public class MyPagerAdapter extends FragmentPagerAdapter {
public List<Fragment> fragments;
public FragmentManager fm;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
this.fm = fm;
this.fragments = new ArrayList<Fragment>();
}
void addFragmentAtPosition(int position, Fragment f) {
if(position == fragments.size())
fragments.add(f);
else
fragments.add(position, f);
notifyDataSetChanged();
}
void removeFragmentAtPosition(int position) {
Fragment f = fragments.remove(position);
if(f != null)
fm.beginTransaction().remove(f).commit();
notifyDataSetChanged();
}
#Override
public Fragment getItem(int position) {
return fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
#Override
public int getItemPosition(Object object){
/*
* called when the fragments are reordered to get the
* changes.
*/
int idx = fragments.indexOf(object);
return idx < 0 ? POSITION_NONE : idx;
}
#Override
public long getItemId(int position) {
/*
* map to a position independent ID, because this
* adapter reorders fragments
*/
return System.identityHashCode(fragments.get(position));
}
}
The key additions are the overrides of public int getItemPosition(Object) and public long getItemId(int). These allow the FragmentPagerAdapter to reposition the existing fragments and to identify the existing active fragments in the FragmentManager cache correctly.
You should not create and add fragments this way. Instead just instantiate the fragments in getItem and the adapter will take care of using them. just do this:
#Override
public Fragment getItem(int position) {
Fragment fragment = new CustomFragment();
fragments.add(fragment)
return fragment
}
I would suggest you don't keep a list of references to fragments since it is not necessary and you risk to create memory leaks.
What i would do is create the fragment only when required like this :
#Override
public Fragment getItem(int position) {
Fragment fragment = new MyFragment();
return fragment;
}
To solve your problem you should create the fragment based on your needs, for example if you have fragments of different class instances like for example one instance of MyFragment another one of YourFragment and so on, just keep a list which says which kind of fragment occupy that position.
For example:
myListMap = new HashMap<Integer, Integer>();
myListMap.put(position, type);
and then create the fragment on the fly:
#Override
public Fragment getItem(int position) {
Fragment fragment = null;
int type = ...find fragment type in that position ....
if(type == MYFRAGMENTTYPE) {
fragment = new MyFragment();
}
return fragment;
}
Don't know if you still need the answer, but I was trying to do something similar and just found the solution :)
You are receiving that exception because of your getItem function. You are returning the fragment in the position that the function receives, and this position is always the last position of the array because that would correspond to the last added fragment in a "typical" usage.
In your case, you want to add a new Fragment in the first position, so your getItem will return twice the same fragment and throw the exception.
To avoid this you need to create a public function into your Adapter and the index of the fragment you are adding, and then return this specific fragment.
PS.: I'm developing only in Kotlin for about 6 months now, so it can have some typos.
public int newFragmentIndex = 0;
public List<Fragment> fragments;
...
public void addFragmentAt(int index, Fragment fragment) {
newFragmentIndex = index;
pageAdapter.fragments.add(index, fragment);
notifyDataSetChanged();
}
public void addFragment(Fragment fragment) {
newFragmentIndex = fragments.size();
pageAdapter.fragments.add(fragment);
notifyDataSetChanged();
}
...
#Override
public Fragment getItem(int position) {
return fragments.get(newFragmentIndex);
}
To call this from your Activity, change your pageAdapter.fragments.add(new CustomFragment());
to
pageAdapter.fragments.addFragment(new CustomFragment());
And
pageAdapter.fragments.add(0, new CustomFragment());
to
pageAdapter.fragments.addFragmentAt(0, new CustomFragment());
Hope this helps you with your problem!

Android fragmentpageradapter update data

Here , I explain my problem. I install a ViewPager and FragmentPagerAdapter so you can slide between two page.
Each fragment contains data ( TextView ) .
When creating my ViewPager with the data passed as a parameter everything goes well.
But if I want to change the data (ie the content of the fragment) it does not refresh
public class MaPagerAdapter extends FragmentPagerAdapter {
private Meteo[]lameteo;
public MaPagerAdapter(FragmentManager fm, Meteo[]lameteo) {
super(fm);
this.lameteo = lameteo;
Log.i("aa", "ville maPagerAdapter"+lameteo[0].getLoc());
}
#Override
public Fragment getItem(int position) {
switch(position) {
case 0: return page_droite.newInstance(lameteo[0]);
case 1: return page_gauche.newInstance(lameteo);
}
return null;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
public void setData(Meteo[] meteo){
this.lameteo=meteo;
}
#Override
public int getCount() {
return 2;
}
}
And my main
if(mPagerAdapter!=null){ //if pagerAdapter already initialized
mPagerAdapter.setData(meteo);
mPagerAdapter.notifyDataSetChanged();
}else
mPagerAdapter = new MaPagerAdapter(getSupportFragmentManager(), meteo);
ViewPager pager = (ViewPager) findViewById(R.id.viewpager);
pager.setAdapter(mPagerAdapter);
Can you help me please ?
No, the pager adapter doesn't work that way. Android doesn't realize that you have that particular data in your fragments, so it wouldn't know how to update it.
You'll have to get references to your fragments inside your setData call, and then you'll have to write new methods inside your fragments that you can call to reset their data. Inside the fragments, you'll have to make sure that you update the fragment's views with the new data. But as for you adapter, you can do something like this:
public class MaPagerAdapter extends FragmentPagerAdapter {
private Meteo[]lameteo;
private FragmentManager fm;
public MaPagerAdapter(FragmentManager fm, Meteo[]lameteo) {
super(fm);
this.fm = fm;
this.lameteo = lameteo;
Log.i("aa", "ville maPagerAdapter"+lameteo[0].getLoc());
}
public void setData(Meteo[] meteo){
this.lameteo=meteo;
page_droite droite = (page_droite) getFragment(0);
droite.setData(meteo[0]);
page_gauche gauche = (page_gauche) getFragment(1);
gauche.setData(meteo);
}
private Fragment getFragment(int position) {
String tag = "android:switcher:" + R.id.viewpager + ":" + getItemId(position);
return fm.findFragmentByTag(tag);
}
Note that there is no need to call notifyDataSetChanged in this case, since you still have the same fragment in your adapter. When you implement the setData functions in your fragments, you need to make all the changes then.

Fragment constantly updated

I've got a FragmentActivity when I instantiate three different (n, n+1, n+2) Fragments.
I need to keep each Fragment updated when user swipes to it, so I used ViewPager.SimpleOnPageChangeListener.onPageSelected in the Fragment Activity, so when user swipes to n+1 or n+2 Fragment and again to n that function update the content.
Without using this workaround if I'm in the Fragment n+1, both n and n+2 are already loaded! I'd like instead that the Fragment load when the user swipes to it, without "pre-load".
This workaround works fine for me but it has a problem: the n Fragment that is the first in the list at start up of the app doesn't load its content. To load its content I have to swipe to n+1 then go back to n.
I know that the content of the Fragment should be setted on the class called at the moment of instantiate the fragment and that extends Fragment class, but in this way I don't know how to keep up to date each Fragment, as I do using onPageSelected.
Any suggestions?
EDIT 1:
I istantiate my fragments in this way in onCreate():
for(int x = 0; x < 3; x++) {
Bundle b = new Bundle();
b.putString( "id" , x );
Fragment myFrag = Fragment.instantiate( myContext , Mm_FragmentPage.class.getName() );
myFrag.setArguments( b );
fragments.add(myFrag);
}
Then I set the adapter in the ViewPager:
mPagerAdapter = new PagerAdapter( super.getSupportFragmentManager() , fragments );
mPager.setAdapter( mPagerAdapter );
Then I use the adapter in the TitlePageIndicator
titleIndicator = (TitlePageIndicator) findViewById( R.id.titleFragments );
titleIndicator.setViewPager( mPager );
titleIndicator.setOnPageChangeListener( new myPageChangeListener() );
And, at the end, the class PagerAdapter:
public class PagerAdapter extends FragmentPagerAdapter
{
// fragments to instantiate in the viewpager
private List<Fragment> fragments;
// constructor
public PagerAdapter(FragmentManager fm, List<Fragment> fragments)
{
super(fm);
this.fragments = fragments;
}
// return access to fragment from position, required override
#Override
public Fragment getItem(int position)
{
return this.fragments.get(position);
}
// number of fragments in list, required override
#Override
public int getCount()
{
return this.fragments.size();
}
#Override
public CharSequence getPageTitle(int position)
{
return getResources().getStringArray( R.array.tab_header_name )[ position ];
}
}
OK, so first thing you need to set OnPageChangeListener on the ViewPager and implement method onPageSelected(int i) and call the adapter's notifyDataSetChanged(), like so:
mPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int i, float v, int i2) {
}
#Override
public void onPageSelected(int i) {
//Tell the adapter that the content was changed
mPager.getAdapter().notifyDataSetChanged();
}
#Override
public void onPageScrollStateChanged(int i) {
}
});
In order to keep the fragments updated, you need to extends FragmentStatePagerAdapter and not FragmentPagerAdapter like what you did. The difference is that with FragmentPagerAdapter the ViewPager will never re-create the fragments, while in FragmentStatePagerAdapter it will.
Then on getItem(..) make sure to return a new instance of the fragment with the new content by passing the content to its arguments via setArguments(). Then override also getItemPosition(..) to tell the adapter that the fragment is not found, and therefore it must re-create it.
public class MyPagerAdapter extends FragmentStatePagerAdapter {
//List to hold the fragments to be shown
//NOTE: It's a list of Fragment classes, not a list of Fragment instances!
private List<Class<? extends Fragment> fragments;
public MyPagerAdapter(FragmentManager fm) {
super(fm);
fragments.add(SomeFragment.class);
fragments.add(AnotherFragment.class);
fragments.add(MoreFragment.class);
}
#Override
public Fragment getItem(int i) {
try {
//Creates a new instance of the fragment
Fragment instance = fragments.get(i).newInstance();
//Put the new content by passing Bundle with new content
instance.setArguments(args);
return instance;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
#Override
public int getItemPosition(Object object) {
//NOTE: you might want to put better logic here
return POSITION_NONE;
}
#Override
public int getCount() {
return pages.size();
}
}
Every time you slide from one fragment to another, onPageSelected() will be fired calling notifyDataSetChanged() which will force the adapter to check also if the position of the fragment has changed. Since we return POSITION_NONE in getItemPosition(..), the adapter thinks that the position changed and will then call getItem(i). In getItem(i) we return a new instance (optionally, passing new arguments). Problem solved! :)
I just tested it by myself, created a small app that have a counter which increases everytime the user slides the page and it works!
This way you can drop the ViewPager.SimpleOnPageChangeListener.onPageSelected.
Learn more about ViewPager.

Referencing Fragments inside ViewPager

I have a problem with referencing my Fragments inside a ViewPager. I would like to do it because from my activity I'd like to refresh a fragment at a specified position (e.g. currently displayed fragment).
Currently I have something like this:
public static class MyPagerAdapter extends FragmentPagerAdapter {
private static final String TAG = "MyPagerAdapter";
private static HashMap<Integer, EventListFragment> mPageReferenceMap = new HashMap<Integer, EventListFragment>();
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
Log.i(TAG, "getItem: "+position);
int dateOffset = position-1;
EventListFragment mFragment = EventListFragment.newInstance(dateOffset);
mPageReferenceMap.put(position, mFragment);
return mFragment;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
Log.i(TAG, "destroyItem: "+position);
mPageReferenceMap.remove(position);
super.destroyItem(container, position, object);
}
public EventListFragment getFragment(int key) {
Log.i(TAG, "Size of pager references: "+mPageReferenceMap.size());
return mPageReferenceMap.get(key);
}
}
The problem is that the destroyItem() gets called more often than getItem(), so I'm left with null references. If I don't use destroyItem() to clear references to destroyed fragments... well I reference fragments that don't exist.
Is there any nice way to reference fragments that are created with EventListFragment mFragment = EventListFragment.newInstance(dateOffset);? Or what should I do to refresh a fragment inside a ViewPager from my activity (from options menu to be precise)?
I managed to solve it. The trick was to make a reference list inside Activity, not PagerAdapter. It goes like this:
List<WeakReference<EventListFragment>> fragList = new ArrayList<WeakReference<EventListFragment>>();
#Override
public void onAttachFragment (Fragment fragment) {
Log.i(TAG, "onAttachFragment: "+fragment);
if(fragment.getClass()==EventListFragment.class){
fragList.add(new WeakReference<EventListFragment>((EventListFragment)fragment));
}
}
public EventListFragment getFragmentByPosition(int position) {
EventListFragment ret = null;
for(WeakReference<EventListFragment> ref : fragList) {
EventListFragment f = ref.get();
if(f != null) {
if(f.getPosition()==position){
ret = f;
}
} else { //delete from list
fragList.remove(f);
}
}
return ret;
}
Of course your fragment has to implement a getPosition() function, but I needed something like this anyway, so it wasn't a problem.
Thanks Alex Lockwood for your suggestion with WeakReference!
Two things:
Add the following line in your Activity's onCreate method (or wherever you initialize your ViewPager):
mPager.setOffscreenPageLimit(NUM_ITEMS-1);
This will keep the additional off-screen pages in memory (i.e. preventing them from being destroyed), even when they aren't currently being shown on the screen.
You might consider implementing your HashMap so that it holds WeakReference<Fragment>s instead of the Fragments themselves. Note that this would require you to change your getFragment method as follows:
WeakReference<Fragment> weakRef = mPageReferenceMap.get(position);
return (weakRef != null) ? weakRef.get() : null;
This has nothing to do with your problem... it's just something I noticed and thought I would bring to your attention. Keeping WeakReferences to your Fragments will allow you to leverage the garbage collector's ability to determine reachability for you, so you don't have to do it yourself.

Categories

Resources