23

When using the FragmentStatePageAdapter I get the fragments like this:

    @Override
    public Fragment getItem(int position) {
        return new SuperCoolFragment();
    }

However, later on my code I need to find this fragment to set a property. In some other parts of the app where I use fragments I basically tag them and look for them using findFragmentByTag(TAG) but with now I don't know how to do it.

How do can I find the fragments using the FragmentStatePageAdapter?

Héctor Júdez Sapena
  • 5,329
  • 3
  • 23
  • 31
  • what about using `final FragmentManager fm = getActivity().getSupportFragmentManager(); final Bundle args = new Bundle(); fm.putFragment(args, TAG, this);` and send this args Bundle to the newInstance of the new fragment? I wonder what happens with the reference (this) to the fragment if the process gets killed & restarted... will the reference inside the persisted args bundle still point to the right instance of the fragment? – Héctor Júdez Sapena Sep 12 '12 at 10:07
  • 1
    I ended up using LocalBroadcastManager to communicate fragments. – Héctor Júdez Sapena Jun 24 '13 at 09:00
  • 1
    shouldn't you simply call yourAdapter.getItem(position) ? – 2cupsOfTech Sep 11 '14 at 21:14
  • I went through all of this and some more answers, but @2cupsOfTech, your comment, is the answer for me! – Froyo Apr 16 '15 at 11:43

10 Answers10

49

* EDIT *

As @ElliotM pointed out, there is a better solution. No need to change anything in your adapter, just get the fragment with:

Fragment myFragment = (Fragment) adapter.instantiateItem(viewPager, viewPager.getCurrentItem());

* OLD SOLUTION *

Best option is the second solution here: http://tamsler.blogspot.nl/2011/11/android-viewpager-and-fragments-part-ii.html

In short: 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..

public Fragment getItem(int index) {
    Fragment myFragment = MyFragment.newInstance();
    mPageReferenceMap.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);
    mPageReferenceMap.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 mPageReferenceMap.get(key);
}

--- EDIT:

Also add this in your adapter, for after an orientation change:

/**
 * After an orientation change, the fragments are saved in the adapter, and
 * I don't want to double save them: I will retrieve them and put them in my
 * list again here.
 */
@Override
public Object instantiateItem(ViewGroup container, int position) {
    MyFragment fragment = (MyFragment) super.instantiateItem(container,
            position);
    mPageReferenceMap.put(position, fragment);
    return fragment;
}
Frank
  • 12,010
  • 8
  • 61
  • 78
  • Good call, second solution from that link is just what I needed for the FragmentStatePagerAdapter. I am just becoming familiar with the ViewPager API and was at a loss for how to keep track. This is the most straightforward way to think about it. – icecreamman Apr 29 '13 at 07:37
  • 1
    I have used a solution like that too and was hoping that the link had something I hadn't done before :( Anyway, it's not bad, I just wish the Adapter would offer a mechanism to access the FragmentManager it uses internally. – Martin Marconcini Sep 30 '13 at 21:03
  • 2
    This does not work if the host activity is destroyed and re-created from the scratch. getItem(index) will not be called and your mPageReferenceMap will be empty. Go to developer options -> "Don't keep Activities" and see for yourself. – Alexey Dec 18 '13 at 18:34
  • I checked my code, it is quit long ago but I think added instantiateItem for this, see above EDIT. Maybe that will fix your problem? – Frank Dec 19 '13 at 08:02
  • 1
    Agreeing with @Alexey, instead of `.getItem` or `.getFragment (your implementation)` you can simply use `MyFragment f49 = (MyFragment) adapter.instantiateItem(pager, 49)` without having to change anything within your adapter. http://stackoverflow.com/a/8886019/2435402. Checking the option `Do not keep activities` in your settings will tell you a LOT about the robustness of your app. – em_ Jun 23 '15 at 21:09
  • You can usually avoid this issue by using listeners on the fragments instead of accessing the fragments after they were created. That way you do not need references to the fragments after they are created. – enl8enmentnow Oct 16 '15 at 21:46
  • Worked for me .............. But is there any better implementation.To handle it effectively. – Justcurious Mar 07 '17 at 13:55
  • Oh I just tried a lot of hacks to achieve this, `adapter.instantiateItem` Did work like a charm. – sud007 Oct 04 '17 at 12:23
15

I've found another way of doing it, not sure if there would be any issues for using it, it seems ok to me.

I was checking at the code for FragmentStatePageAdapter and I saw this method:

@Override
52     public Object More ...instantiateItem(View container, int position) {
53         // If we already have this item instantiated, there is nothing
54         // to do.  This can happen when we are restoring the entire pager
55         // from its saved state, where the fragment manager has already
56         // taken care of restoring the fragments we previously had instantiated.
57         if (mFragments.size() > position) {
58             Fragment f = mFragments.get(position);
59             if (f != null) {
60                 return f;
61             }
62         }
63 
64         if (mCurTransaction == null) {
65             mCurTransaction = mFragmentManager.beginTransaction();
66         }
67 
68         Fragment fragment = getItem(position);
69         if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
70         if (mSavedState.size() > position) {
71             Fragment.SavedState fss = mSavedState.get(position);
72             if (fss != null) {
73                 fragment.setInitialSavedState(fss);
74             }
75         }
76         while (mFragments.size() <= position) {
77             mFragments.add(null);
78         }
79         fragment.setMenuVisibility(false);
80         mFragments.set(position, fragment);
81         mCurTransaction.add(container.getId(), fragment);
82 
83         return fragment;
84     }

You can use this method to get the fragment already instated for that particular position. So if you want to get Fragment at position 1, you just need to call:

myFragmentStatePageAdpater.instantiateItem(null, 1)

Hope that helps

Roberto
  • 1,793
  • 1
  • 18
  • 19
  • Given that there is no direct way to get the Fragment and all solutions are hacks and depend on source code of FragmentStatePageAdapter, I like this one the best. Its clean and without any problems. Although, I'll recommend to use this to only fetch the current Fragment. Updating an invisible fragment from another fragment anyways seems like a bad design to me. – marrock Jan 03 '16 at 04:16
5

I implemented this by adding a getItemTag(int position) function to FragmentStatePagerAdapter. A drop-in version of the updated file is here if anyone else wants to go this route.

Chris Lacy
  • 4,222
  • 3
  • 35
  • 33
  • This works for me and seems easier to implement than adding a map and worrying about state changes. Only thing I used the v13 version of FragmentStatePagerAdapter and I made the getItemTag(int position) an abstract method. – d370urn3ur Aug 04 '14 at 09:00
2

Solution with storing fragments to a HashMap in your getItem() method does not work if your activity is re-created!

Problem is, default implementation of the FragmentStatePagerAdapter restores fragments directly from the FragmentManager without calling the getItem() method. This means that you'll never get a chance to store a reference to your fragment.

For more info, see source code: FragmentStatePagerAdapter#restoreState.

This basically means that the only way to get a reference to your fragment is by resorting to reflections. This is relatively* safe** in case youre using support library.

Here's how:

    @SuppressWarnings("unchecked")
    public Fragment getFragment(int position) {
        try {
            Field f = FragmentStatePagerAdapter.class.getDeclaredField("mFragments");
            f.setAccessible(true);
            ArrayList<Fragment> fragments = (ArrayList<Fragment>) f.get(this);
            if (fragments.size() > position) {
                return fragments.get(position);
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

relatively* means that its not as dangerous as using reflection on framework classes. Support classes are compiled into your app and there is no way that this code will work on your device but will crash on some other device. You can still potentially break this solution by updating support library to a newer version

safe** its never really safe to use reflections; however you have to resort to this method when a library you're using has design flaws.

Alexey
  • 7,262
  • 4
  • 48
  • 68
  • 2
    Yes you are correct this is an issue when Activity and FragmentStatePagerAdapter are recreated. There is a disconnect. I recommend two safer alternatives 1.) Design it so Activity and Fragments don't need to know about each other or 2.) When activity is being recreated and after FragmentStatePagerAdapter is newed up, call notifyDataSetChanged() to force getItem() to be called. – LEO Jan 31 '14 at 17:37
1

As @Lancelot mentioned, You may just call and cast result to your SuperCoolFragment:

SuperCoolFragment frag = (SuperCoolFragment) yourFragmentStatePageAdpater.instantiateItem(null, position); 
Arvis
  • 8,273
  • 5
  • 33
  • 46
1

While using FragmentStateAdapter setting up custom tag and looking for it is pretty simple. This is how FragmentStateAdapter tags its fragments:

mFragmentManager.beginTransaction()
    .add(fragment, "f" + holder.getItemId())
    .setMaxLifecycle(fragment, STARTED)
    .commitNow();

As you can see it's a combination of static string "f" and getItemId(). After overriding fun getItemId(position: Int): Long in your adapter, finding a specific fragment should be straightforward to do. For example

override fun getItemId(position: Int): Long {
    return if(position == 0) {
            200001
    }else 200002
}

The tag for our first fragment would be "fmy_first_fragment" and found with childFragmentManager.findFragmentByTag("fmy_first_fragment") as? MyFirstFragment

Careful whenever you look for fragment, you have to use the same FragmentManager as was used to create it. So I recommend creating your adapter with FragmentManager and LifeCycle as a constructor.

Jakub Kostka
  • 551
  • 2
  • 7
  • 20
0

I tried Frank's answer that suggests Second Approach from the tutorial linked in it, bot for some reason after adding method getFragment() to my ViewPagerAdapter I can't access it via mViewPager.getAdapter.getFragment() as if it doesn't exist, so I moved mPageReferenceMap declaration from the ViewPagerAdapter to the enclosing class so it is easily accessed from any point of the enclosing class (in my case it is MainActivity's callback method from other fragment) which finally makes it possible to pass data between fragments. Thanks to Frank and feel free to implement my fixture if you like me face issue with accessing custom getFragment method.

Ivan V
  • 3,024
  • 4
  • 26
  • 36
0

Another solution is to copy paste the source code of FragmentStatePagerAdapter and add a getFragment method:

public Fragment getFragment(int position) {
    if (position >= mFragments.size()) {
        return null;
    }
    return mFragments.get(position);
}
Romain Piel
  • 11,017
  • 15
  • 71
  • 106
0

Override setPrimaryItem, and using that position, it would be the right page index:

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container,position,object);
        if (position == 1) {
            currentPageIndex=cameraFragment.pageIndex;
}
Efferalgan
  • 1,681
  • 1
  • 14
  • 24
Guang
  • 1
-10

Create a holder to hold the new SuperCoolFragment object. Then tag and return the holder object.

@Override
public Fragment getItem(int position) {
    SuperCoolFragment superCoolFragment = new SuperCoolFragment();
    superCoolFragment.setTag(YOUR_TAG);
    return superCoolFragment;
}
Hang Guan
  • 102
  • 5