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

时间:2012-12-26 01:07:56

标签: android android-fragments fragmentpageradapter

我有问题让我的片段通过使用Activity的{​​{1}}作为辅助类来实现对象的管理以及连接{{1的所有细节与关联的FragmentPagerAdapter相关联。我已经实现了ViewPager,就像Android示例项目 Support4Demos 所提供的那样。

主要问题是当我没有Id或Tag时,如何从TabHost获取特定片段? FragmentPagerAdapter正在创建片段并自动生成Id和标签。

15 个答案:

答案 0 :(得分:171)

问题摘要

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

如果您正在阅读此内容,您可能已经知道FragmentPagerAdapter / FragmentStatePagerAdapter旨在为您的Fragments创建ViewPager,但在活动娱乐时(无论是来自设备轮换或系统终止您的应用以重新获得内存)这些Fragments将不会再次创建,而是创建instances retrieved from the FragmentManager。现在说你的Activity需要获得对这些Fragments的引用才能对它们进行处理。由于id set them internally,您创建的tag没有FragmentsFragmentPagerAdapter。所以问题是如何在没有这些信息的情况下获得对它们的引用......

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

我在此问题和类似问题上看到的很多解决方案都依赖于通过调用Fragment和模仿internally created tag: "android:switcher:" + viewId + ":" + id来获取对现有FragmentManager.findFragmentByTag()的引用。这样做的问题在于你依赖于内部源代码,我们都知道并不能保证它们永远保持不变。 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 1}}以同样的方式: 注意:这不适用于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更改,您仍然可以安全。


不要忘记,根据您Activity的设计,您尝试工作的Fragments可能存在,也可能不存在,所以你有在使用您的参考文献之前,通过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...
}

答案 1 :(得分:79)

我根据以下帖子找到了我的问题的答案:reusing fragments in a fragmentpageradapter

我学到的东西很少:

    getItem(int position)中的
  1. FragmentPagerAdapter对于此方法实际执行的操作具有相当误导性的名称。它创建新片段,而不是返回现有片段。在这个意义上,该方法应该重命名为Android SDK中的createItem(int position)。所以这种方法无法帮助我们获取碎片。
  2. 根据帖子support FragmentPagerAdapterholds reference to old fragments中的说明,您应该将片段的创建留给FragmentPagerAdapter,这意味着您没有参考片段或其标签。如果您有片段标记,则可以通过调用FragmentManager轻松地从findFragmentByTag()检索对它的引用。我们需要一种方法来找出给定页面位置的片段标记。
  3. <强>解决方案

    在您的类中添加以下辅助方法以检索片段标记并将其发送到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

答案 2 :(得分:11)

我这样做的方法是定义WeakReferences的Hashtable,如下所示:

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

这似乎运作良好,我发现它比

更少hacky
"android:switcher:" + viewId + ":" + position

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

如果有人发现这种方法有问题,那么评论非常受欢迎。

答案 3 :(得分: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;
}

答案 4 :(得分:10)

您不需要覆盖Visibility,也不需要依赖于使用内部instantiateItem方法创建片段代码的兼容性。 makeFragmentName是一种公开方法,因此您可以(实际上应该)在您的活动的instantiateItem方法中调用它,以获取对如果需要,可以将碎片存储在本地变量中。请记住用一组onCreate电话包围 instantiateItem javadoc中描述的startUpdatefinishUpdate方法:

  

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

例如,这是在PagerAdapter方法中存储对标签片段的引用的方法:

onCreate

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方法创建新的方法,并且&#34;存储&#34;它们在getItem以供将来使用。

其他一些信息:
如果您未在FragmentManager方法中调用instantiateItem / startUpdate所包围的finishUpdate,那么您将冒险将您的片段实例永远不会提交< / em>到onCreate:当您的活动变为前景FragmentManager时,系统会自动调用instantiateItem来获取您的碎片,但startUpdate / finishUpdate 可能 < em> not (取决于实施细节)他们基本上做的是开始/提交FragmentTransaction
可能导致对创建的片段实例的引用非常快速地丢失(例如,当您旋转屏幕时)并且重新创建的频率远远超过必要时间。取决于&#34; heavy&#34;你的碎片,它可能会产生不可忽视的性能后果 但更重要的是,在这种情况下,存储在本地变量上的片段实例将变得陈旧:由于android平台无法从FragmentManager获取相同的实例,因此它可能会创建并使用新的实例,而您的变量仍然会引用旧的。

答案 5 :(得分: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;
    }
}

答案 6 :(得分:2)

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

这是一种不依赖于FragmentPagerAdapter实现来获取标记的方法。覆盖instantiateItem(),它将返回从getItem()创建的片段或从片段管理器中找到的片段。

{{1}}

答案 7 :(得分:2)

当我需要访问子片段或主要(当前可见)片段时,我总是使用这个基类。它不依赖于任何实现细节,它会关注生命周期的变化,因为在两种情况下都会调用覆盖的方法 - 创建新的片段实例时以及从FragmentManager接收实例时。

public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter {

    private final ArrayList<Fragment> mFragments;
    private Fragment mPrimaryFragment;

    public FragmentPagerAdapterExt(FragmentManager fm) {
        super(fm);
        mFragments = new ArrayList<>(getCount());
    }

    @Override public Object instantiateItem(ViewGroup container, int position) {
        Object object = super.instantiateItem(container, position);
        mFragments.add((Fragment) object);
        return object;
    }

    @Override public void destroyItem(ViewGroup container, int position, Object object) {
        mFragments.remove(object);
        super.destroyItem(container, position, object);
    }

    @Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
        mPrimaryFragment = (Fragment) object;
    }

    /** Returns currently visible (primary) fragment */
    public Fragment getPrimaryFragment() {
        return mPrimaryFragment;
    }

    /** Returned list can contain null-values for not created fragments */
    public List<Fragment> getFragments() {
        return Collections.unmodifiableList(mFragments);
    }
}

答案 8 :(得分:1)

我设法通过使用ID而不是标记来解决此问题。 (我正在使用我定义的FragmentStatePagerAdapter,它使用我的自定义片段,我在其中覆盖onAttach方法,在那里你将id保存在某处:

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

然后你只需在活动中轻松访问片段:

Fragment f = getSupportFragmentManager.findFragmentById(fragId);

答案 9 :(得分:0)

有关从FragmentPagerAdapter返回片段的信息,请参阅this post。依赖于您知道片段的索引 - 但这将在getItem()中设置(仅在实例化时)

答案 10 :(得分: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);
}

此方法将实际可见片段附加到活动而不创建新片段。

答案 11 :(得分:0)

不确定我的方法是否是正确或最好的方法,因为我是Java / Android的相对初学者,但它确实有效(我确信它违反了面向对象的原则但没有其他解决方案适用于我的用例)。

我有一个使用带有FragmentStatePagerAdapter的ViewPager的托管活动。为了获得FragmentStatePagerAdapter创建的Fragments的引用,我在片段类中创建了一个回调接口:

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中当前存在的片段。

答案 12 :(得分: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);
    }
}

答案 13 :(得分:0)

FragmentStateAdapter for ViewPager2 UPDATE

FragmentStateAdaptercreateFragment() 而不是 getItem()。而且没有instantiateView()这样的方法。

因此,在配置更改后重新创建托管 Activity/Fragment 时,不会调用 createFragment()。这意味着不会再次创建适配器中的片段,而是从 FragmentManager 检索它们的实例。 因此,如果您需要对片段进行一些处理,只需从 FragmentManager 获取它们即可。

适配器创建:

CustomFragmentAdapter adapter = new CustomFragmentAdapter(getChildFragmentManager(), getLifecycle());

FragmentManager/Activity 重新加载后从 Fragment 检索片段:

    FragmentManager manager = getChildFragmentManager();
    ArrayList<Fragment> fragments = new ArrayList<>(manager.getFragments());

    for (Fragment fr : fragments) {
        
        // do something
    }

答案 14 :(得分:-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;
     }
}