How to get existing fragments when using FragmentP

2019-01-01 12:24发布

I have problem making my fragments communicating with each other through the Activity, which is using the FragmentPagerAdapter, as a helper class that implements the management of tabs and all details of connecting a ViewPager with associated TabHost. I have implemented FragmentPagerAdapter just as same as it is provided by the Android sample project Support4Demos.

The main question is how can I get particular fragment from FragmentManager when I don't have neither Id or Tag? FragmentPagerAdapter is creating the fragments and auto generating the Id and Tags.

14条回答
无色无味的生活
2楼-- · 2019-01-01 12:48

the solution suggested by @personne3000 is nice, but it has one problem: when activity goes to the background and gets killed by the system (in order to get some free memory) and then restored, the fragmentReferences will be empty, because getItem wouldn't be called.

The class below handles such situation:

public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {

    public static final String FRAGMENT_SAVE_PREFIX = "holder";
    private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters.

    public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
        fragmentManager = fm;
    }

    private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();

    protected void holdFragment(F fragment) {
        holdFragment(holder.size(), fragment);
    }

    protected void holdFragment(int position, F fragment) {
        if (fragment != null)
            holder.put(position, new WeakReference<F>(fragment));
    }

    public F getHoldedItem(int position) {
        WeakReference<F> ref = holder.get(position);
        return ref == null ? null : ref.get();
    }

    public int getHolderCount() {
        return holder.size();
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation
        super.restoreState(state, loader);
        Bundle bundle = (Bundle) state;
        for (String key : bundle.keySet()) {
            if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
                int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
                Fragment f = fragmentManager.getFragment(bundle, key);
                holdFragment(index, (F) f);
            }
        }
    }

    @Override
    public Parcelable saveState() {
        Bundle state = (Bundle) super.saveState();
        if (state == null)
            state = new Bundle();

        for (int i = 0; i < holder.size(); i++) {
            int id = holder.keyAt(i);
            final F f = getHoldedItem(i);
            String key = FRAGMENT_SAVE_PREFIX + id;
            fragmentManager.putFragment(state, key, f);
        }
        return state;
    }
}
查看更多
姐姐魅力值爆表
3楼-- · 2019-01-01 12:52

Summary of the problem

Note: In this answer I'm going to reference FragmentPagerAdapter and its source code. But the general solution should also apply to FragmentStatePagerAdapter.

If you're reading this you probably already know that FragmentPagerAdapter/FragmentStatePagerAdapter is meant to create Fragments for your ViewPager, but upon Activity recreation (whether from a device rotation or the system killing your App to regain memory) these Fragments won't be created again, but instead their instances retrieved from the FragmentManager. Now say your Activity needs to get a reference to these Fragments to do work on them. You don't have an id or tag for these created Fragments because FragmentPagerAdapter set them internally. So the problem is how to get a reference to them without that information...

Problem with current solutions: relying on internal code

A lot of the solutions I've seen on this and similar questions rely on getting a reference to the existing Fragment by calling FragmentManager.findFragmentByTag() and mimicking the internally created tag: "android:switcher:" + viewId + ":" + id. The problem with this is that you're relying on internal source code, which as we all know is not guaranteed to remain the same forever. The Android engineers at Google could easily decide to change the tag structure which would break your code leaving you unable to find a reference to the existing Fragments.

Alternate solution without relying on internal tag

Here's a simple example of how to get a reference to the Fragments returned by FragmentPagerAdapter that doesn't rely on the internal tags set on the Fragments. The key is to override instantiateItem() and save references in there instead of in getItem().

public class SomeActivity extends Activity {
    private FragmentA m1stFragment;
    private FragmentB m2ndFragment;

    // other code in your Activity...

    private class CustomPagerAdapter extends FragmentPagerAdapter {
        // other code in your custom FragmentPagerAdapter...

        public CustomPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // Do NOT try to save references to the Fragments in getItem(),
            // because getItem() is not always called. If the Fragment
            // was already created then it will be retrieved from the FragmentManger
            // and not here (i.e. getItem() won't be called again).
            switch (position) {
                case 0:
                    return new FragmentA();
                case 1:
                    return new FragmentB();
                default:
                    // This should never happen. Always account for each position above
                    return null;
            }
        }

        // Here we can finally safely save a reference to the created
        // Fragment, no matter where it came from (either getItem() or
        // FragmentManger). Simply save the returned Fragment from
        // super.instantiateItem() into an appropriate reference depending
        // on the ViewPager position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
            // save the appropriate reference depending on position
            switch (position) {
                case 0:
                    m1stFragment = (FragmentA) createdFragment;
                    break;
                case 1:
                    m2ndFragment = (FragmentB) createdFragment;
                    break;
            }
            return createdFragment;
        }
    }

    public void someMethod() {
        // do work on the referenced Fragments, but first check if they
        // even exist yet, otherwise you'll get an NPE.

        if (m1stFragment != null) {
            // m1stFragment.doWork();
        }

        if (m2ndFragment != null) {
            // m2ndFragment.doSomeWorkToo();
        }
    }
}

or if you prefer to work with tags instead of class member variables/references to the Fragments you can also grab the tags set by FragmentPagerAdapter in the same manner: NOTE: this doesn't apply to FragmentStatePagerAdapter since it doesn't set tags when creating its Fragments.

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
    // get the tags set by FragmentPagerAdapter
    switch (position) {
        case 0:
            String firstTag = createdFragment.getTag();
            break;
        case 1:
            String secondTag = createdFragment.getTag();
            break;
    }
    // ... save the tags somewhere so you can reference them later
    return createdFragment;
}

Note that this method does NOT rely on mimicking the internal tag set by FragmentPagerAdapter and instead uses proper APIs for retrieving them. This way even if the tag changes in future versions of the SupportLibrary you'll still be safe.


Don't forget that depending on the design of your Activity, the Fragments you're trying to work on may or may not exist yet, so you have to account for that by doing null checks before using your references.

Also, if instead you're working with FragmentStatePagerAdapter, then you don't want to keep hard references to your Fragments because you might have many of them and hard references would unnecessarily keep them in memory. Instead save the Fragment references in WeakReference variables instead of standard ones. Like this:

WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
    // reference hasn't been cleared yet; do work...
}
查看更多
美炸的是我
4楼-- · 2019-01-01 12:54

This class do the trick without relying on internal tags. Warning: Fragments should be accessed using the getFragment method and not the getItem one.

public class ViewPagerAdapter extends FragmentPagerAdapter {

    private final Map<Integer, Reference<Fragment>> fragments = new HashMap<>();
    private final List<Callable0<Fragment>> initializers = new ArrayList<>();
    private final List<String> titles = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    void addFragment(Callable0<Fragment> initializer, String title) {
        initializers.add(initializer);
        titles.add(title);
    }

    public Optional<Fragment> getFragment(int position) {
        return Optional.ofNullable(fragments.get(position).get());
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment =  initializers.get(position).execute();
        return fragment;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        fragments.put(position, new WeakReference<>(fragment));
        return fragment;
    }

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

    @Override
    public CharSequence getPageTitle(int position) {
        return titles.get(position);
    }
}
查看更多
怪性笑人.
5楼-- · 2019-01-01 12:56

I managed to solve this issue by using ids instead of tags. (I am using I defined FragmentStatePagerAdapter which uses my custom Fragments in which I overrode the onAttach method, where you save the id somewhere:

@Override
public void onAttach(Context context){
    super.onAttach(context);
    MainActivity.fragId = getId();
}

And then you just access the fragment easily inside the activity:

Fragment f = getSupportFragmentManager.findFragmentById(fragId);
查看更多
其实,你不懂
6楼-- · 2019-01-01 12:57

you don't need to override instantiateItem nor rely on compatibility of creating fragment tags with internal makeFragmentName method. instantiateItem is a public method so you can (and actually you should) call it in onCreate method of your activity to get references to instances of your fragments and store them on local vars if you need. Just remember to surround a set of instantiateItem calls with startUpdate and finishUpdate methods as described in PagerAdapter javadoc:

A call to the PagerAdapter method startUpdate(ViewGroup) indicates that the contents of the ViewPager are about to change. One or more calls to instantiateItem(ViewGroup, int) and/or destroyItem(ViewGroup, int, Object) will follow, and the end of an update will be signaled by a call to finishUpdate(ViewGroup).

So for example this is the way to store references to your tab fragments in onCreate method:

public class MyActivity extends AppCompatActivity {

    Fragment0 tab0; Fragment1 tab1;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.myLayout);
        ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
        MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
        viewPager.setAdapter(adapter);
        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.setupWithViewPager(viewPager);

        adapter.startUpdate(viewPager);
        tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
        tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
        adapter.finishUpdate(viewPager);
    }

    class MyPagerAdapter extends FragmentPagerAdapter {

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

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

        @Override public Fragment getItem(int position) {
            if (position == 0) return new Fragment0();
            if (position == 1) return new Fragment1();
            return null;  // or throw some exception
        }

        @Override public CharSequence getPageTitle(int position) {
            if (position == 0) return getString(R.string.tab0);
            if (position == 1) return getString(R.string.tab1);
            return null;  // or throw some exception
        }
    }
}

instantiateItem will first try to get references to existing fragment instances from FragmentManager. Only if they don't exist yet, it will create new ones using getItem method from your adapter and "store" them in the FragmentManager for any future use.

Some additional info:
If you don't call instantiateItem surrounded by startUpdate/finishUpdate in your onCreate method then you are risking that your fragment instances will never be committed to FragmentManager: when your activity becomes foreground instantiateItem will be called automatically to obtain your fragments, but startUpdate/finishUpdate may not (depending on implementation details) and what they basically do is begin/commit a FragmentTransaction.
This may result in references to the created fragment instances being lost very quickly (for example when you rotate your screen) and recreated much more often than necessary. Depending on how "heavy" your fragments are, it may have a non-negligible performance consequences.
More importantly however, in such case instances of fragments stored on local vars will become stale: since android platform was not able to obtain the same instances from FragmentManager it may create and use new ones, while your vars will still be referencing the old ones.

查看更多
栀子花@的思念
7楼-- · 2019-01-01 12:57

Not sure if my method was the correct or best way to do this since I am a relative beginner with Java/Android, but it did work (I'm sure it violates object oriented principles but no other solution worked for my use case).

I had a hosting Activity that was using a ViewPager with a FragmentStatePagerAdapter. In order to get references to the Fragments that were created by FragmentStatePagerAdapter I created a callback interface within the fragment class:

public interface Callbacks {
    public void addFragment (Fragment fragment);
    public void removeFragment (Fragment fragment);
}

In the hosting activity I implemented the interface and created a LinkedHasSet to keep track of the fragments:

public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {

    private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();

    @Override
    public void addFragment (Fragment fragment) {
        mFragments.add(fragment);
    }

    @Override
    public void removeFragment (Fragment fragment) {
        mFragments.remove(fragment);
    }
}

Within the ViewPagerFragment class I added the fragments to the list within onAttach and removed them within onDetach:

public class ViewPagerFragment extends Fragment {

    private Callbacks mCallbacks;

    public interface Callbacks {
        public void addFragment (Fragment fragment);
        public void removeFragment (Fragment fragment);
    } 

    @Override
    public void onAttach (Context context) {
        super.onAttach(context);
        mCallbacks = (Callbacks) context;
        // Add this fragment to the HashSet in the hosting activity
        mCallbacks.addFragment(this);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Remove this fragment from the HashSet in the hosting activity
        mCallbacks.removeFragment(this);
        mCallbacks = null;
    }
}

Within the hosting activity you'll now be able to use mFragments to iterate through the fragments that currently exist in the FragmentStatePagerAdapter.

查看更多
登录 后发表回答