Update data in ListFragment as part of ViewPager
Asked Answered
A

10

141

I'm using the v4 compatibility ViewPager in Android. My FragmentActivity has a bunch of data which is to be displayed in different ways on different pages in my ViewPager. So far I just have 3 instances of the same ListFragment, but in the future I will have 3 instances of different ListFragments. The ViewPager is on a vertical phone screen, the lists are not side-by-side.

Now a button on the ListFragment starts an separate full-page activity (via the FragmentActivity), which returns and FragmentActivity modifies the data, saves it, then attempts to update all views in its ViewPager. It is here, where I am stuck.

public class ProgressMainActivity extends FragmentActivity
{
    MyAdapter mAdapter;
    ViewPager mPager;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
    ...
        mAdapter = new MyAdapter(getSupportFragmentManager());

        mPager = (ViewPager) findViewById(R.id.viewpager);
        mPager.setAdapter(mAdapter);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        ...
        updateFragments();
        ...
    }
    public void updateFragments()
    {
        //Attempt 1:
        //mAdapter.notifyDataSetChanged();
        //mPager.setAdapter(mAdapter);

        //Attempt 2:
        //HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentById(mAdapter.fragId[0]);
        //fragment.updateDisplay();
    }

    public static class MyAdapter extends FragmentPagerAdapter implements
         TitleProvider
    {
      int[] fragId = {0,0,0,0,0};
      public MyAdapter(FragmentManager fm)
      {
         super(fm);
      }
      @Override
      public String getTitle(int position){
         return titles[position];
      }
      @Override
      public int getCount(){
         return titles.length;
      }

      @Override
      public Fragment getItem(int position)
      {

         Fragment frag = HomeListFragment.newInstance(position);
         //Attempt 2:
         //fragId[position] = frag.getId();
         return frag;
      }

      @Override
      public int getItemPosition(Object object) {
         return POSITION_NONE; //To make notifyDataSetChanged() do something
     }
   }

    public class HomeListFragment extends ListFragment
    {
    ...
        public static HomeListFragment newInstance(int num)
        {
            HomeListFragment f = new HomeListFragment();
            ...
            return f;
        }
   ...

Now as you can see, my first attempt was to notifyDataSetChanged on the entire FragmentPagerAdapter, and this showed to update the data sometimes, but others I got an IllegalStateException: Can not perform this action after onSaveInstanceState.

My second attempt involed trying to call an update function in my ListFragment, but getId in getItem returned 0. As per the docs I tried by

acquiring a reference to the Fragment from FragmentManager, using findFragmentById() or findFragmentByTag()

but I don't know the tag or id of my Fragments! I have an android:id="@+id/viewpager" for ViewPager, and a android:id="@android:id/list" for my ListView in the ListFragment layout, but I don't think these are useful.

So, how can I either: a) update the entire ViewPager safely in one go (ideally returning the user to the page he was on before) - it is ok that the user see the view change. Or preferably, b) call a function in each affected ListFragment to update the ListView manually.

Any help would be gratefully accepted!

Adobe answered 11/9, 2011 at 15:17 Comment(0)
C
58

Try to record the tag each time a Fragement is instantiated.

public class MPagerAdapter extends FragmentPagerAdapter {
    private Map<Integer, String> mFragmentTags;
    private FragmentManager mFragmentManager;

    public MPagerAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
        mFragmentTags = new HashMap<Integer, String>();
    }

    @Override
    public int getCount() {
        return 10;
    }

    @Override
    public Fragment getItem(int position) {
        return Fragment.instantiate(mContext, AFragment.class.getName(), null);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object obj = super.instantiateItem(container, position);
        if (obj instanceof Fragment) {
            // record the fragment tag here.
            Fragment f = (Fragment) obj;
            String tag = f.getTag();
            mFragmentTags.put(position, tag);
        }
        return obj;
    }

    public Fragment getFragment(int position) {
        String tag = mFragmentTags.get(position);
        if (tag == null)
            return null;
        return mFragmentManager.findFragmentByTag(tag);
    }
}
Cumulative answered 24/8, 2012 at 6:44 Comment(8)
This look OK but it does not work in my class. Does it work with you? I still get null for fragment.Haslet
It works for me, i used it in several projects. You can try to read the source code of FragmentPagerAdapter and print some logs to debug.Cumulative
Thanks works like a charm even after rotating screens.Even i was able to change content of FragmentA using MyActivity from FragmentB,using this solution.Whiteley
I think this is one of the best ways to do this. Elegant and simple, and it works for both FragmentPagerAdapter and (I think) FragmentStatePagerAdapter. Additionally it does not hold references to the fragments, so it allows the framework to manage GC, etcWeslee
Works great for me, and I am puzzled as to why far fewer people have accepted this solution over the one that relies on the internal implementation of makeFragmentName().Seena
It works for FragmentPagerAdaper but fails for FragmentStatePagerAdaper (Fragment.getTag() always return null.Anisomerous
Can you comment on getItem? I thought getItem was called by instantiateItem. In this case how is position used?Spirant
I tried this solution, created a method to update fragment's data. But now I am not able to see the views of fragment. I can see the data is going to fragment but views are not visible. Any help?Carlin
P
257

Barkside's answer works with FragmentPagerAdapter but doesn't work with FragmentStatePagerAdapter, because it doesn't set tags on fragments it passes to FragmentManager.

With FragmentStatePagerAdapter it seems we can get by, using its instantiateItem(ViewGroup container, int position) call. It returns reference to fragment at position position. If FragmentStatePagerAdapter already holds reference to fragment in question, instantiateItem just returns reference to that fragment, and doesn't call getItem() to instantiate it again.

So, suppose, I'm currently looking at fragment #50, and want to access fragment #49. Since they are close, there's a good chance the #49 will be already instantiated. So,

ViewPager pager = findViewById(R.id.viewpager);
FragmentStatePagerAdapter a = (FragmentStatePagerAdapter) pager.getAdapter();
MyFragment f49 = (MyFragment) a.instantiateItem(pager, 49)
Pacorro answered 16/1, 2012 at 20:44 Comment(5)
I would simplify that by just making "a" a PagerAdapter (PagerAdapter a = pager.getAdapter();). Or even MyFragment f49 = (MyFragment) pager.getAdapter().instantiateItem(pager, 49); instantiateItem() is from PagerAdapter, so you don't have to typecast it to call instantiateItem()Sampan
... and just in case anyone is wondering, the ViewPager does keep track of the index of the viewed fragment (ViewPager.getCurrentItem()), which is the 49 in the example above... but I've gotta say that I'm AMAZED that the ViewPager API won't return a reference to the actual Fragment directly.Enaenable
With FragmentStatePagerAdapter, using above code with a.instantiateItem(viewPager, viewPager.getCurrentItem()); (to get rid of the 49) as proposed by @Enaenable works perfectly.Fionnula
Wait, so I have to pass the ViewPager to every fragment it contains just to be able to do things to the views in the fragment? Currently everything I do in onCreate on the current page happens on the next page. This sounds extremely shady leak-wise.Greaten
it's important to mention that any calls to instantiateItem should be surrounded by startUpdate/finishUpdate calls. details are in my answer to a similar question: #14035590Anecdotage
A
154

OK, I think I've found a way to perform request b) in my own question so I'll share for others' benefit. The tag of fragments inside a ViewPager is in the form "android:switcher:VIEWPAGER_ID:INDEX", where VIEWPAGER_ID is the R.id.viewpager in XML layout, and INDEX is the position in the viewpager. So if the position is known (eg 0), I can perform in updateFragments():

      HomeListFragment fragment = 
          (HomeListFragment) getSupportFragmentManager().findFragmentByTag(
                       "android:switcher:"+R.id.viewpager+":0");
      if(fragment != null)  // could be null if not instantiated yet
      {
         if(fragment.getView() != null) 
         {
            // no need to call if fragment's onDestroyView() 
            //has since been called.
            fragment.updateDisplay(); // do what updates are required
         }
      }

I've no idea if this is a valid way of doing it, but it'll do until something better is suggested.

Adobe answered 12/9, 2011 at 20:18 Comment(10)
I logged in just to +1 your answer and question.Proof
since this isn't in the official documentation, I wouldn't use it. What if they change the tag in a future release?Tolman
FragmentPagerAdapter comes with a static method makeFragmentName used to generate the Tag that you could use instead for a slightly-less-hacky approach: HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentByTag(FragmentPagerAdapter.makeFragmentName(R.id.viewpager, 0));Collative
why don't you add it as answer?Electrophysiology
@dgmltn, makeFragmentName is private static method, so you cannot get access to it.Synchro
Have you found a better solution for this?Evanish
See my answer below for a non-scary way of solving this, based on IOSched2012 source code.Rank
Sweet mother of Jesus this works! I'll just keep my finger crossed and use it.Kildare
This is absolutely terrible and hacky. You can't rely on this as the Android framework might change. Instead, manage the fragments yourself, something like this: gist.github.com/nesquena/c715c9b22fb873b1d259 That said, it's terrible that the Android framework doesn't provide a better API. @AlexLockwood you don't seriously think it's ok that people are using the above solution do you?Bryantbryanty
This is the better and best solution for data refresh or for other option. Save the daySeptember
C
58

Try to record the tag each time a Fragement is instantiated.

public class MPagerAdapter extends FragmentPagerAdapter {
    private Map<Integer, String> mFragmentTags;
    private FragmentManager mFragmentManager;

    public MPagerAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
        mFragmentTags = new HashMap<Integer, String>();
    }

    @Override
    public int getCount() {
        return 10;
    }

    @Override
    public Fragment getItem(int position) {
        return Fragment.instantiate(mContext, AFragment.class.getName(), null);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object obj = super.instantiateItem(container, position);
        if (obj instanceof Fragment) {
            // record the fragment tag here.
            Fragment f = (Fragment) obj;
            String tag = f.getTag();
            mFragmentTags.put(position, tag);
        }
        return obj;
    }

    public Fragment getFragment(int position) {
        String tag = mFragmentTags.get(position);
        if (tag == null)
            return null;
        return mFragmentManager.findFragmentByTag(tag);
    }
}
Cumulative answered 24/8, 2012 at 6:44 Comment(8)
This look OK but it does not work in my class. Does it work with you? I still get null for fragment.Haslet
It works for me, i used it in several projects. You can try to read the source code of FragmentPagerAdapter and print some logs to debug.Cumulative
Thanks works like a charm even after rotating screens.Even i was able to change content of FragmentA using MyActivity from FragmentB,using this solution.Whiteley
I think this is one of the best ways to do this. Elegant and simple, and it works for both FragmentPagerAdapter and (I think) FragmentStatePagerAdapter. Additionally it does not hold references to the fragments, so it allows the framework to manage GC, etcWeslee
Works great for me, and I am puzzled as to why far fewer people have accepted this solution over the one that relies on the internal implementation of makeFragmentName().Seena
It works for FragmentPagerAdaper but fails for FragmentStatePagerAdaper (Fragment.getTag() always return null.Anisomerous
Can you comment on getItem? I thought getItem was called by instantiateItem. In this case how is position used?Spirant
I tried this solution, created a method to update fragment's data. But now I am not able to see the views of fragment. I can see the data is going to fragment but views are not visible. Any help?Carlin
S
35

If you ask me, the second solution on the below page, keeping track of all the "active" fragment pages, is better: http://tamsler.blogspot.nl/2011/11/android-viewpager-and-fragments-part-ii.html

The answer from barkside is too hacky for me.

you keep track of all the "active" fragment pages. In this case, you keep track of the fragment pages in the FragmentStatePagerAdapter, which is used by the ViewPager.

private final SparseArray<Fragment> mPageReferences = new SparseArray<Fragment>();

public Fragment getItem(int index) {
    Fragment myFragment = MyFragment.newInstance();
    mPageReferences.put(index, myFragment);
    return myFragment;
}

To avoid keeping a reference to "inactive" fragment pages, we need to implement the FragmentStatePagerAdapter's destroyItem(...) method:

public void destroyItem(View container, int position, Object object) {
    super.destroyItem(container, position, object);
    mPageReferences.remove(position);
}

... and when you need to access the currently visible page, you then call:

int index = mViewPager.getCurrentItem();
MyAdapter adapter = ((MyAdapter)mViewPager.getAdapter());
MyFragment fragment = adapter.getFragment(index);

... where the MyAdapter's getFragment(int) method looks like this:

public MyFragment getFragment(int key) {
    return mPageReferences.get(key);
}

"

Stonecutter answered 11/8, 2012 at 12:7 Comment(5)
@Stonecutter second solution in the link is simple...!Thanks..:)Honniball
Better if you use a SparseArray instead of a mapMaldives
or SparseArrayCompatAppurtenance
updated my answer so it uses a SparseArray, if you target < 11, use a SparseArrayCompat instead because the class what updated in version 11.Stonecutter
This will break on lifecycle events. See https://mcmap.net/q/57287/-retrieve-a-fragment-from-a-viewpagerAppurtenance
R
13

Okay, after testing the method by @barkside above, I could not get it to work with my application. Then I remembered that the IOSched2012 app uses a viewpager as well, and that is where I found my solution. It does not use any fragment ID's or Tags as these are not stored by viewpager in an easily accessible way.

Here's the important parts from the IOSched apps HomeActivity. Pay particular attention to the comment, as therein lies the key.:

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

    // Since the pager fragments don't have known tags or IDs, the only way to persist the
    // reference is to use putFragment/getFragment. Remember, we're not persisting the exact
    // Fragment instance. This mechanism simply gives us a way to persist access to the
    // 'current' fragment instance for the given fragment (which changes across orientation
    // changes).
    //
    // The outcome of all this is that the "Refresh" menu button refreshes the stream across
    // orientation changes.
    if (mSocialStreamFragment != null) {
        getSupportFragmentManager().putFragment(outState, "stream_fragment",
                mSocialStreamFragment);
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (mSocialStreamFragment == null) {
        mSocialStreamFragment = (SocialStreamFragment) getSupportFragmentManager()
                .getFragment(savedInstanceState, "stream_fragment");
    }
}

And store instances of you Fragments in the FragmentPagerAdapter like so:

    private class HomePagerAdapter extends FragmentPagerAdapter {
    public HomePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return (mMyScheduleFragment = new MyScheduleFragment());

            case 1:
                return (mExploreFragment = new ExploreFragment());

            case 2:
                return (mSocialStreamFragment = new SocialStreamFragment());
        }
        return null;
    }

Also, remember to guard your Fragment calls like so:

    if (mSocialStreamFragment != null) {
        mSocialStreamFragment.refresh();
    }
Rank answered 14/1, 2013 at 2:26 Comment(2)
Ryan, Is this to say that you are overiding the functions that the ViewPager uses to 'keep' your fragment instances and setting them to a public variable that is accesible? Why on restore instance do you have if (mSocialStreamFragment == null) { ?Kare
this is a nice answer and +1 for io12 app ref - unsure if this will work across lifecycle changes though due to getItem() not being called after the parent is recreated due to a lifecycle change. See https://mcmap.net/q/57287/-retrieve-a-fragment-from-a-viewpager. In the example this is partly guarded against for one fragment but not the other twoAppurtenance
B
2

You can copy FragmentPagerAdapter and modify some source code, add getTag() method

for example

public abstract class AppFragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;

private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;

public AppFragmentPagerAdapter(FragmentManager fm) {
    mFragmentManager = fm;
}


public abstract Fragment getItem(int position);

@Override
public void startUpdate(ViewGroup container) {
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);


    String name = getTag(position);
    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,
                getTag(position));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment) object).getView());
    mCurTransaction.detach((Fragment) object);
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitAllowingStateLoss();
        mCurTransaction = null;
        mFragmentManager.executePendingTransactions();
    }
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return ((Fragment) object).getView() == view;
}

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}


public long getItemId(int position) {
    return position;
}

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

protected abstract String getTag(int position);
}

then extend it, override these abstract method,don't need to be afraid of Android Group change

FragmentPageAdapter source code in the future

 class TimeLinePagerAdapter extends AppFragmentPagerAdapter {


    List<Fragment> list = new ArrayList<Fragment>();


    public TimeLinePagerAdapter(FragmentManager fm) {
        super(fm);
        list.add(new FriendsTimeLineFragment());
        list.add(new MentionsTimeLineFragment());
        list.add(new CommentsTimeLineFragment());
    }


    public Fragment getItem(int position) {
        return list.get(position);
    }

    @Override
    protected String getTag(int position) {
        List<String> tagList = new ArrayList<String>();
        tagList.add(FriendsTimeLineFragment.class.getName());
        tagList.add(MentionsTimeLineFragment.class.getName());
        tagList.add(CommentsTimeLineFragment.class.getName());
        return tagList.get(position);
    }


    @Override
    public int getCount() {
        return list.size();
    }


}
Berseem answered 11/9, 2012 at 13:22 Comment(0)
F
1

Alternatively you can override setPrimaryItem method from FragmentPagerAdapter like so:

public void setPrimaryItem(ViewGroup container, int position, Object object) { 
    if (mCurrentFragment != object) {
        mCurrentFragment = (Fragment) object; //Keep reference to object
        ((MyInterface)mCurrentFragment).viewDidAppear();//Or call a method on the fragment
    }

    super.setPrimaryItem(container, position, object);
}

public Fragment getCurrentFragment(){
    return mCurrentFragment;
}
Fatwitted answered 19/10, 2014 at 4:24 Comment(0)
D
1

Also works without problems:

somewhere in page fragment's layout:

<FrameLayout android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone" android:id="@+id/fragment_reference">
     <View android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone"/>
</FrameLayout>

in fragment's onCreateView():

...
View root = inflater.inflate(R.layout.fragment_page, container, false);
ViewGroup ref = (ViewGroup)root.findViewById(R.id.fragment_reference);
ref.setTag(this);
ref.getChildAt(0).setTag("fragment:" + pageIndex);
return root;

and method to return Fragment from ViewPager, if exists:

public Fragment getFragment(int pageIndex) {        
        View w = mViewPager.findViewWithTag("fragment:" + pageIndex);
        if (w == null) return null;
        View r = (View) w.getParent();
        return (Fragment) r.getTag();
}
Dynamic answered 17/7, 2015 at 19:40 Comment(0)
O
0

I want to give my approach in case it can help anyone else:

This is my pager adapter:

 public class CustomPagerAdapter extends FragmentStatePagerAdapter{
    private Fragment[] fragments;

    public CustomPagerAdapter(FragmentManager fm) {
        super(fm);
        fragments = new Fragment[]{
                new FragmentA(),
                new FragmentB()
        };
    }

    @Override
    public Fragment getItem(int arg0) {
        return fragments[arg0];
    }

    @Override
    public int getCount() {
        return fragments.length;
    }

}

In my activity I have:

public class MainActivity {
        private ViewPager view_pager;
        private CustomPagerAdapter adapter;


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

        adapter = new CustomPagerAdapter(getSupportFragmentManager());
        view_pager = (ViewPager) findViewById(R.id.pager);
        view_pager.setAdapter(adapter);
        view_pager.setOnPageChangeListener(this);
          ...
         }

}

Then to get the current fragment what I do is:

int index = view_pager.getCurrentItem();
Fragment currentFragment = adapter.getItem(index);
Once answered 28/10, 2013 at 22:35 Comment(1)
this will break after activity / fragment lifecycle methodsAppurtenance
M
0

This is my solution since I don't need to keep track of my tabs and need to refresh them all anyway.

    Bundle b = new Bundle();
    b.putInt(Constants.SharedPreferenceKeys.NUM_QUERY_DAYS,numQueryDays);
    for(android.support.v4.app.Fragment f:getSupportFragmentManager().getFragments()){
        if(f instanceof HomeTermFragment){
            ((HomeTermFragment) f).restartLoader(b);
        }
    }
Marta answered 25/2, 2018 at 18:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.