使用FragmentPagerAdapter时如何获取现有片段


99

我在使片段通过Activity(使用FragmentPagerAdapter)作为帮助程序类相互通信时遇到困难,该帮助程序类实现了选项卡的管理以及将a ViewPager和关联的关联的所有详细信息TabHost。我已经实现FragmentPagerAdapter了与Android示例项目Support4Demos提供的相同的方法

主要问题是,FragmentManager当我既没有ID也没有Tag时,如何获得特定片段?FragmentPagerAdapter正在创建片段并自动生成ID和代码。



@ jk2K这怎么可能是一年后被问到的问题的重复
Davi,2018年

@Dawit像标签一样重复,与时间无关,以后的问题有更高的见解
jk2K

这里的大多数答案都不适用于生产环境,因此请在stackoverflow.com/a/54280113/2413303中
EpicPandaForce

Answers:


193

问题总结

注意:在这个答案中,我将参考FragmentPagerAdapter及其源代码。但是一般的解决方案也应该适用于FragmentStatePagerAdapter

如果您正在阅读此书,您可能已经知道FragmentPagerAdapter/ FragmentStatePagerAdapterFragments为您创建的ViewPager,但是在Activity重新创建(无论是设备旋转还是系统杀死您的App以重新获得内存)之后,这些Fragments都不会再次创建,而是创建它们从中检索到的实例FragmentManager。现在说您Activity需要参考这些内容Fragments以进行处理。您不具有idtag为这些创建Fragments,因为FragmentPagerAdapter 它们设置在内部。所以问题是如何在没有这些信息的情况下获得对它们的引用...

当前解决方案的问题:依靠内部代码

我在此问题和类似问题上看到的许多解决方案都依赖于Fragment通过调用FragmentManager.findFragmentByTag()和模仿内部创建的标记"android:switcher:" + viewId + ":" + id来获得对现有标记的引用。这样做的问题是您依赖内部源代码,众所周知,内部源代码不能保证永远保持不变。Google的Android工程师可以轻松地决定更改tag结构,这将破坏您的代码,使您无法找到现有的引用Fragments

无需内部解决方案的替代解决方案 tag

这是一个简单的示例,该示例说明如何获取对的Fragments返回引用FragmentPagerAdapter,而不依赖于上的内部tags集合Fragments。关键是要覆盖instantiateItem()和保存的引用在那里,而不是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();
        }
    }
}

或者,如果您更喜欢使用tags而不是类的成员变量/引用,则Fragments也可以tags通过FragmentPagerAdapter相同的方式获取该集合:注意:这不适用于FragmentStatePagerAdapter该集合,因为tags在创建它时未设置它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;
}

请注意,此方法不依赖于模仿内部tag集合FragmentPagerAdapter,而是使用适当的API来检索它们。这样,即使您tag将来的版本发生更改,SupportLibrary您仍然可以安全使用。


不要忘记,根据您的设计ActivityFragments您正在尝试进行的工作可能存在或可能不存在,因此您必须null在使用引用之前进行检查以解决这一问题。

另外,如果您正在使用FragmentStatePagerAdapter,则您不想保留对您的硬引用,Fragments因为您可能有很多硬引用,而硬引用将不必要地将它们保留在内存中。而是将Fragment引用保存在WeakReference变量中,而不是标准引用中。像这样:

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...
}

3
这是一个非常好的解决方案,但似乎失去其有效性,如果你不知道有多少碎片将被传入做。
防暴云纬

3
@Zorpix,您可以将创建的片段存储在HashMap中:map.put(position,createdFragment);
Tom Bevelander,2015年

12
这值得勾选!实现这一目标的非常聪明和全面的方法。您对我帮助很大,谢谢!
young_souvlaki

2
起初,此解决方案看起来过于复杂,因此我跳过了它。不过,我终于回到了问题的答案,因为其他答案并不令人满意。这并不像我想的那么难。
Suragch '16

1
不需要覆盖任何东西,实际上你应该覆盖instantiateItem。要做到这一点的正确方法是调用 instantiateItemonCreate方法,您的活动包围startUpdatefinishUpdate。详见我的答案
morgwai

82

我根据以下帖子找到了我的问题的答案:在fragmentpageradapter中重用碎片

我学到的东西很少:

  1. getItem(int position)FragmentPagerAdapter什么这种方法实际上做的是相当误导性名称。它创建新的片段,而不返回现有的片段。因此,该方法应重命名为createItem(int position)Android SDK中的类似名称。因此,此方法无助于获取碎片。
  2. 根据后期支持中的解释,FragmentPagerAdapters保留对旧片段的引用,您应该将片段的创建留给FragmentPagerAdapter和,这意味着您没有对Fragments或其标签的引用。如果您有片段标记,则可以FragmentManager通过调用轻松地从中检索对其的引用findFragmentByTag()。我们需要一种在给定页面位置找出片段标签的方法。

在您的类中添加以下帮助程序方法以检索片段标记并将其发送给该findFragmentByTag()方法。

private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
     return "android:switcher:" + viewPagerId + ":" + fragmentPosition;
}

注意!这与FragmentPagerAdapter创建新片段时使用的方法相同。看到此链接http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/android/support/v2/app/FragmentPagerAdapter.java#104


顺便说一句,在这个问答中有更多关于这个话题的信息:stackoverflow.com/questions/6976027/…–
Thomas

1
viewId输入参数是什么?哪个观点?
Nilzor 2014年

@Nilzor viewId是ViewPager的ID。
Dr.jacky 2014年

也许不是猜测片段可以将其标签告知其活动的标签onAttach()
盆地

4
这不是正确的方法。@Tony Chan的答案是最好和正确的方法。
Morteza Rastgoo,2015年

16

您不需要通过手动创建片段标签来覆盖instantiateItem或依赖内部makeFragmentName方法的兼容性。
instantiateItem是一个公共方法,因此您可以并且实际上应该onCreate您的活动方法中调用它,startUpdatefinishUpdate使用对PagerAdapter javadoc中所述的和方法的调用来包围它:

对PagerAdapter方法startUpdate(ViewGroup)的调用指示ViewPager的内容即将更改。随后将对instantiateItem(ViewGroup,int)和/或destroyItem(ViewGroup,int,Object)进行一次或多次调用,并通过对finishUpdate(ViewGroup)的调用来发出更新结束的信号。

然后,通过上述方式,可以根据需要将对片段实例的引用存储在本地var上。参见示例:

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) findViewById(R.id.tabs)).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首先将尝试从中获取对现有片段实例的引用FragmentManager。仅当它们尚不存在时,它将使用getItem适配器中的方法创建新的,并将其“存储”在其中FragmentManager以供将来使用。

需要特别注意的是,即使您不需要获取对片段的引用,也应该在方法中instantiateItem使用startUpdate/ 包围所有选项卡,如下所示:finishUpdateonCreate

    adapter.startUpdate(viewPager);
    // ignoring return values of the below 2 calls, just side effects matter:
    adapter.instantiateItem(viewPager, 0);
    adapter.instantiateItem(viewPager, 1);
    adapter.finishUpdate(viewPager);

如果你不这样做,那么你冒着你的片断实例将永远不会被提交FragmentManager:当你的活动变得前景instantiateItem将自动被调用,以获得您的片段,但startUpdate/ finishUpdate 可能 不会(取决于实施细则)和他们基本上做的是开始/提交一个FragmentTransaction
可能导致对创建的片段实例的引用很快丢失(例如,在旋转屏幕时),并且重新创建的次数比必要的要多得多。根据片段的“沉重”程度,它可能会产生不可忽略的性能后果。
而且,在这种情况下,片段存储在本地变量中的实例可能变得陈旧:如果android平台FragmentManager出于任何原因尝试从中获取它们,它将失败并因此将创建并使用新的,而您的var仍将引用旧的var。


1
在某些情况下,这可能是最好的解决方案。但是,如果FragmentManger杀死碎片并重新整理碎片会怎样?
woltran

1
@woltran FragmentManager不能只是随机杀死(在这里销毁是正确的词)Fragment(想想如果它决定杀死Fragment当前显示的a会发生什么;))。通常,a的生命周期Fragment与其绑定Activity(有关详细信息,请参见github.com/xxv/android-lifecycle)-> a Fragment仅在其Activity销毁后才能销毁。在这种情况下,当用户导航回给ActivityonCreate将会再次调用和的一个新的实例Fragment将被创建。
morgwai

这是真正的答案
MJ Studio

例如,您是否应该真正创建片段,而不是依赖于用户在滚动ViewPager时创建片段?
Yar

@Yar是的,您确实应该。我提供的文档摘录清楚地说明了这一点,“一些其他信息”部分说明了原因。
morgwai

11

我这样做的方式是定义WeakReferences哈希表,如下所示:

protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;

然后,我像这样编写了getItem()方法:

@Override
public Fragment getItem(int position) {

    Fragment fragment;
    switch(position) {
    case 0:
        fragment = new MyFirstFragmentClass();
        break;

    default:
        fragment = new MyOtherFragmentClass();
        break;
    }

    fragmentReferences.put(position, new WeakReference<Fragment>(fragment));

    return fragment;
}

然后,您可以编写一个方法:

public Fragment getFragment(int fragmentId) {
    WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
    return ref == null ? null : ref.get();
}

这似乎很好用,但我发现它比

"android:switcher:" + viewId + ":" + position

技巧,因为它不依赖于如何实现FragmentPagerAdapter。当然,如果片段已由FragmentPagerAdapter释放,或者尚未创建,则getFragment将返回null。

如果有人发现此方法有问题,欢迎发表评论。


int fragmentId应该重命名为int position
lmaooooo,2015年

7
我使用的是非常相似的方法。但是,当从savedState捆绑包创建寻呼机时,此操作将失败。例如:活动进入后台,并在调用onSavedStateInstance()之后回到前台。在那种情况下,不会调用getItem()方法。
Anoop

由于FragmentManager中已经有一个总是up2date的地图,因此创建自己的地图的原因是什么?有关详细信息,请参见我的答案。
morgwai

同样,片段已被破坏的事实并不能保证没有对其的强有力引用(尽管可能,但不能保证),在这种情况下,您的地图仍将包含陈旧的片段。
morgwai

1
系统重新创建片段后,这将无法正常工作。
EpicPandaForce

10

我创建了此方法,该方法正在为我提供对当前片段的引用。

public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
    try {
        Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
        Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
        f.setAccessible(true);
        FragmentManager fm = (FragmentManager) f.get(adapter);
        m.setAccessible(true);
        String tag = null;
        tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
        return fm.findFragmentByTag(tag);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } 
    return null;
}

很高兴记住要在方法之外创建方法和字段以获得更好的性能
Marcos Vasconcelos

2

@ personne3000建议的解决方案很好,但是有一个问题:当活动进入后台并被系统杀死(为了获得一些可用内存)然后恢复时,该结果fragmentReferences将为空,因为getItem不会叫。

下面的类处理这种情况:

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;
    }
}

1

获取片段的主要障碍是您不能依赖getItem()。更改方向后,对片段的引用将为null,并且不会再次调用getItem()。

这是一种不依赖FragmentPagerAdapter的实现来获取标签的方法。重写InstantiateItem(),它将返回从getItem()创建或从片段管理器找到的片段。

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Object value =  super.instantiateItem(container, position);

    if (position == 0) {
        someFragment = (SomeFragment) value;
    } else if (position == 1) {
        anotherFragment = (AnotherFragment) value;
    }

    return value;
}

0

这个职位从FragmentPagerAdapter返回片段。确实依赖于您知道片段的索引-但这将在getItem()中设置(仅在实例化时)


0

我设法通过使用id而不是标签来解决此问题。(我使用的是我定义的FragmentStatePagerAdapter,它使用了我的自定义Fragments,其中我覆盖了onAttach方法,将id保存在某个地方:

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

然后,您可以轻松地在活动内部访问该片段:

Fragment f = getSupportFragmentManager.findFragmentById(fragId);

0

我不知道这是否是最好的方法,但是没有其他方法对我有用。包括getActiveFragment在内的所有其他选项均返回null或导致应用程序崩溃。

我注意到在屏幕旋转时,片段被附着了,所以我用它将片段发送回活动。

在片段中:

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    try {
        mListener = (OnListInteractionListener) activity;
        mListener.setListFrag(this);
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString()
                + " must implement OnFragmentInteractionListener");
    }
}

然后在活动中:

@Override
public void setListFrag(MyListFragment lf) {
    if (mListFragment == null) {
        mListFragment = lf;
    }
}

最后在活动onCreate()中:

if (savedInstanceState != null) {
    if (mListFragment != null)
        mListFragment.setListItems(items);
}

这种方法将实际的可见片段附加到活动,而无需创建新片段。


0

由于我是Java / Android的相对入门者,因此不确定我的方法是正确的方法还是最佳方法,但是它确实有效(我确定它违反了面向对象的原理,但没有其他解决方案适用于我的用例)。

我有一个托管活动,正在使用带有FragmentStatePagerAdapter的ViewPager。为了获得对由FragmentStatePagerAdapter创建的Fragment的引用,我在fragment类中创建了一个回调接口:

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

在托管活动中,我实现了接口并创建了LinkedHasSet来跟踪片段:

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);
    }
}

在ViewPagerFragment类中,我将片段添加到onAttach的列表中,并在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;
    }
}

在托管活动中,您现在可以使用mFragments遍历FragmentStatePagerAdapter中当前存在的片段。


0

此类无需依赖内部标签即可完成操作。警告:应该使用getFragment方法而不是getItem方法来访问片段。

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

继续尝试这段代码,

public class MYFragmentPAdp extends FragmentPagerAdapter {

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

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

     @Override
     public Fragment getItem(int position) {
         if (position == 0)
             Fragment fragment = new Fragment1();
         else (position == 1)
             Fragment fragment = new Fragment2();
         return fragment;
     }
}

就像我在下面的答案中解释的那样,纳吉布getItem()正在创建新片段,而不是返回现有片段,因为人们可能期望给定名称get和not create。在同一篇文章中查看我的解决方案。
Ismar Slomic

片段片段= new YourCustomFragmentClass(); 在这里写检查。
纳吉布·艾哈迈德·普塔瓦拉2012年

我还是不明白这个变化,你是在代替创建新片段的事实让现有的..
ISMAR Slomic

您仍然只初始化并返回自定义片段,例如Fragment fragment = new YourFragment();。返回片段
纳吉布·艾哈迈德·普塔瓦拉2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.