使用 FragmentPagerAdapter 时如何获取现有片段
Posted
技术标签:
【中文标题】使用 FragmentPagerAdapter 时如何获取现有片段【英文标题】:How to get existing fragments when using FragmentPagerAdapter 【发布时间】:2012-12-11 16:29:27 【问题描述】:我的片段通过Activity
相互通信时遇到问题,它使用FragmentPagerAdapter
作为实现选项卡管理的辅助类以及将ViewPager
与关联@987654326 连接的所有详细信息@。我已经实现了 FragmentPagerAdapter
,就像 android 示例项目 Support4Demos 提供的一样。
主要问题是当我既没有 ID 也没有标签时,如何从 FragmentManager
获取特定片段? FragmentPagerAdapter
正在创建片段并自动生成 Id 和标签。
【问题讨论】:
Getting the current Fragment instance in the viewpager的可能重复 @jk2K 这怎么可能与 1 年后提出的问题重复 @Dawit 像标签一样重复,与时间无关,后面的问题有更高的看法 这里的大部分答案在生产中都不起作用,所以请在***.com/a/54280113/2413303中查看我的答案 【参考方案1】:问题总结
注意:在这个答案中,我将参考 FragmentPagerAdapter
及其源代码。但是一般的解决方案也应该适用于FragmentStatePagerAdapter
。
如果您正在阅读本文,您可能已经知道FragmentPagerAdapter
/FragmentStatePagerAdapter
旨在为您的ViewPager
创建Fragments
,但在活动重新创建时(无论是设备轮换还是系统杀死您的应用)恢复记忆)这些Fragments
不会再次创建,而是它们的instances retrieved from the FragmentManager
。现在说你的Activity
需要参考这些Fragments
来处理它们。对于这些创建的Fragments
,您没有id
或tag
,因为FragmentPagerAdapter
set them internally。所以问题是如何在没有这些信息的情况下获得对它们的引用......
当前解决方案的问题:依赖内部代码
我在这个问题和类似问题上看到的许多解决方案都依赖于通过调用FragmentManager.findFragmentByTag()
并模仿internally created tag: "android:switcher:" + viewId + ":" + id
来获取对现有Fragment
的引用。这样做的问题是您依赖于内部源代码,众所周知,不能保证永远保持不变。 Google 的 Android 工程师可以轻松决定更改 tag
结构,这会破坏您的代码,导致您无法找到对现有 Fragments
的引用。
不依赖内部tag
的替代解决方案
这是一个简单示例,说明如何获取对 FragmentPagerAdapter
返回的 Fragments
的引用,该引用不依赖于 Fragments
上设置的内部 tags
。关键是覆盖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
的引用,您也可以以相同的方式获取FragmentPagerAdapter
设置的tags
:
注意:这不适用于FragmentStatePagerAdapter
,因为它在创建Fragments
时没有设置tags
。
@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;
请注意,此方法不依赖于模仿FragmentPagerAdapter
设置的内部tag
,而是使用适当的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...
【讨论】:
这确实是一个很好的解决方案,但是如果你不知道会传入多少个碎片,它似乎就失去了效力。 @Zorpix 您可以将创建的片段存储在 HashMap 中:map.put(position, createdFragment); 这值得勾选!非常聪明和全面的方法来实现这一点。你帮了我很多谢谢! 起初这个解决方案看起来太复杂了,所以我跳过了它。不过,我终于回到了它,因为其他答案并不令人满意。并没有我想象的那么难。 不需要覆盖任何东西,你实际上不应该覆盖instantiateItem
。正确的方法是在onCreate
被startUpdate
和finishUpdate
包围的活动的instantiateItem
方法中调用 instantiateItem
。详情见我的回答【参考方案2】:
我根据以下帖子找到了我的问题的答案:reusing fragments in a fragmentpageradapter
我学到的一些东西:
FragmentPagerAdapter
中的 getItem(int position)
是这个方法实际作用的相当误导的名称。它创建新的片段,而不是返回现有的片段。在这个意义上,该方法应该在 Android SDK 中重命名为 createItem(int position)
之类的东西。所以这个方法并不能帮助我们获取碎片。
根据support FragmentPagerAdapterholds reference to old fragments 帖子中的解释,您应该将片段的创建留给FragmentPagerAdapter
,这意味着您没有参考片段或其标签。如果您有片段标签,您可以通过调用findFragmentByTag()
从FragmentManager
轻松检索对它的引用。我们需要一种方法来找出给定页面位置的片段标签。
解决方案
在您的类中添加以下辅助方法以检索片段标记并将其发送到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
【讨论】:
顺便说一句,这个问答中有更多关于这个话题的内容:***.com/questions/6976027/… viewId 输入参数是什么?哪个视图? @Nilzor viewId 是 ViewPager 的 id。 也许片段无需猜测标签,而是将其标签告知其在onAttach()
中的活动?
这不是正确的方法。 @Tony Chan 的回答是最好和正确的方法。【参考方案3】:
您不需要覆盖instantiateItem
也不需要通过手动创建片段标签来依赖与内部makeFragmentName
方法的兼容性。instantiateItem
是一个 public 方法,因此您可以并且实际上应该在您的活动的onCreate
方法中调用它,并调用startUpdate
和finishUpdate
方法,如PagerAdapter
javadoc 中所述:
对 PagerAdapter 方法 startUpdate(ViewGroup) 的调用表明 ViewPager 的内容即将更改。随后将调用一次或多次实例化Item(ViewGroup, int) 和/或destroyItem(ViewGroup, int, Object),并通过调用finishUpdate(ViewGroup) 发出更新结束信号。
然后,如果需要,您可以通过上述方式将片段实例的引用存储在本地变量上。见例子:
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
中以供将来使用。
重要的是要注意,即使您不需要获取对片段的引用,您仍然应该在 onCreate
方法中为 startUpdate
/finishUpdate
包围的所有选项卡调用 instantiateItem
,例如这个:
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
获取它们,它将失败,因此将创建和使用新的片段,而您的 vars 仍将引用旧的。
【讨论】:
在某些情况下它可能是最好的解决方案。但是如果 FragmentManger 会杀死片段并重新创建它会发生什么? @woltranFragmentManager
不能随便kill(destroy 是正确的词)你的Fragment
(想想会发生什么如果它决定杀死当前正在显示的Fragment
;) )。通常,Fragment
的生命周期与其Activity
绑定(详见github.com/xxv/android-lifecycle)-> 只有Activity
被销毁,Fragment
才能被销毁。在这种情况下,当用户导航回给定的Activity
时,将再次调用其onCreate
并创建Fragment
的新实例。
这是真正的答案
您真的应该创建片段,而不是依赖于在用户滚动 ViewPager 时创建片段吗?
@Yar 是的,你真的应该这样做。我提供的文档摘录清楚地说明了这一点,“一些附加信息”部分解释了原因。【参考方案4】:
我这样做的方法是定义一个 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();
这似乎运作良好,我发现它比
"android:switcher:" + viewId + ":" + position
trick,因为它不依赖于 FragmentPagerAdapter 的实现方式。 当然如果fragment已经被FragmentPagerAdapter释放或者还没有被创建,getFragment会返回null。
如果有人发现这种方法有问题,欢迎使用 cmets。
【讨论】:
int fragmentId
应重命名为 int position
我使用了一种非常相似的方法。但是当从一个 savedState 包创建寻呼机时,这会失败。例如: Activity 进入后台并在调用 onSavedStateInstance() 后返回前台。在这种情况下,不会调用 getItem() 方法。
创建自己的地图的原因是什么,因为 FragmentManager 中已经有一个总是更新的?详情见我的回答。
另外,片段已被销毁这一事实,并不能保证没有对它的强引用(尽管很有可能,但 NOT 保证)在这种情况下,您的地图仍会包含陈旧的片段。
这在系统重新创建 Fragments 后将无法正常工作。【参考方案5】:
我创建了这个方法,它可以让我获得对当前片段的引用。
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;
【讨论】:
很高兴记住在方法之外创建方法和字段以获得更好的性能【参考方案6】:ViewPager2 的 FragmentStateAdapter 更新
FragmentStateAdapter
具有 createFragment()
而不是 getItem()
。而且没有instantiateView()
这样的方法。
因此,在配置更改后重新创建托管 Activity
/Fragment
时,不会调用 createFragment()
。这意味着不再创建适配器中的片段,而是从FragmentManager
检索它们的实例。
所以如果你需要对碎片做一些工作,你可以简单地从FragmentManager
获取它们。
适配器创建:
CustomFragmentAdapter adapter = new CustomFragmentAdapter(getChildFragmentManager(), getLifecycle());
在Activity
/Fragment
重新加载后从FragmentManager
检索片段:
FragmentManager manager = getChildFragmentManager();
ArrayList<Fragment> fragments = new ArrayList<>(manager.getFragments());
for (Fragment fr : fragments)
// do something
【讨论】:
【参考方案7】:@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;
【讨论】:
【参考方案8】:获取片段句柄的主要障碍是您不能依赖 getItem()。方向更改后,对片段的引用将为 null,并且不会再次调用 getItem()。
这是一种不依赖实现 FragmentPagerAdapter 来获取标签的方法。覆盖 instanceiteItem() ,它将返回从 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;
【讨论】:
【参考方案9】:请参阅this post 从 FragmentPagerAdapter 返回片段。是否依赖于您知道片段的索引 - 但这将在 getItem() 中设置(仅在实例化时)
【讨论】:
【参考方案10】:我设法通过使用 ids 而不是标签来解决这个问题。 (我正在使用我定义的 FragmentStatePagerAdapter,它使用我的自定义片段,在其中我覆盖了 onAttach 方法,您将 id 保存在某处:
@Override
public void onAttach(Context context)
super.onAttach(context);
MainActivity.fragId = getId();
然后您就可以轻松地在 Activity 中访问片段:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
【讨论】:
【参考方案11】:我不知道这是否是最好的方法,但没有其他方法对我有用。 包括 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);
这种方法将实际可见的片段附加到 Activity 而不创建新片段。
【讨论】:
【参考方案12】:不确定我的方法是正确还是最好的方法,因为我是 Java/Android 的相对初学者,但它确实有效(我确信它违反了面向对象的原则,但没有其他解决方案适合我的使用案例)。
我有一个使用 ViewPager 和 FragmentStatePagerAdapter 的托管 Activity。为了获得对 FragmentStatePagerAdapter 创建的片段的引用,我在片段类中创建了一个回调接口:
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 中当前存在的片段。
【讨论】:
【参考方案13】:这个类不依赖内部标签就可以解决问题。警告:应该使用 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);
【讨论】:
【参考方案14】:继续尝试这段代码,
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;
【讨论】:
Najib,就像我在下面的回答中解释的那样,getItem() 正在创建新片段,而不是返回现有片段,因为人们可能期望给定名称 get 而不是 create 。在同一篇文章中查看我的解决方案。 片段片段 = new YourCustomFragmentClass();写在这里检查一下。 我仍然不明白这如何改变您正在创建新片段而不是获取现有片段的事实.. 您仍然初始化并仅返回您的自定义片段,例如 Fragment fragment = new YourFragment();返回片段;以上是关于使用 FragmentPagerAdapter 时如何获取现有片段的主要内容,如果未能解决你的问题,请参考以下文章
在 FragmentPagerAdapter 上调用 getItem() 时,它返回一个空片段
FragmentPagerAdapter加载fragment并使用setUserVisibleHint()处理预加载时遇到的坑,给textview赋值时出现的空指针异常
FragmentPagerAdapter与FragmentStatePagerAdapter区别
使用 FragmentPagerAdapter 时不调用 Fragment onResume()