Как получить существующие фрагменты при использовании FragmentPagerAdapter
У меня проблема с сообщением друг с другом общими фрагментами через Activity
, который использует FragmentPagerAdapter
, в качестве вспомогательного класса, который реализует управление вкладками и все подробности подключения ViewPager
с ассоциированным TabHost
. Я реализовал FragmentPagerAdapter
так же, как и в примере с Android-проектом Support4Demos.
Главный вопрос: как я могу получить определенный фрагмент из FragmentManager
, когда у меня нет ни Id, ни тега? FragmentPagerAdapter
создает фрагменты и автоматически генерирует идентификаторы и теги.
Ответы
Ответ 1
Резюме проблемы
Примечание. В этом ответе я буду ссылаться на FragmentPagerAdapter
и его исходный код. Но общее решение должно также применяться к FragmentStatePagerAdapter
.
Если вы читаете это, вы, вероятно, уже знаете, что FragmentPagerAdapter
/FragmentStatePagerAdapter
предназначен для создания Fragments
для вашего ViewPager
, но при активном отдыхе (будь то от поворота устройства или системы, убивающей ваше приложение для восстановления памяти) эти Fragments
не будут созданы снова, а вместо них экземпляры, извлеченные из FragmentManager
. Теперь скажите, что ваш Activity
должен получить ссылку на эти Fragments
, чтобы работать над ними. У вас нет id
или tag
для этих созданных Fragments
, потому что FragmentPagerAdapter
установить их внутренне. Поэтому проблема заключается в том, как получить ссылку на них без этой информации...
Проблема с текущими решениями: опираясь на внутренний код
Многие решения, которые я видел на этом и подобные вопросы, полагаются на получение ссылки на существующий Fragment
, вызывая FragmentManager.findFragmentByTag()
и имитируя внутренне созданный тег: "android:switcher:" + viewId + ":" + id
. Проблема заключается в том, что вы полагаетесь на внутренний исходный код, который, как мы все знаем, не гарантирует, что он останется неизменным навсегда. Инженеры Android в Google могут легко решить изменить структуру 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
, поскольку при создании 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;
}
Обратите внимание, что этот метод НЕ полагается на имитацию внутреннего 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...
}
Ответ 2
Я нашел ответ на свой вопрос, основанный на следующем посте: повторное использование фрагментов в фрагменте pageradapter
Несколько вещей, которые я узнал:
-
getItem(int position)
в FragmentPagerAdapter
- это довольно вводящее в заблуждение название того, что на самом деле делает этот метод. Создает новые фрагменты, а не возвращает существующие. В этом смысле метод должен быть переименован во что-то вроде createItem(int position)
в Android SDK. Так что этот метод не помогает нам получать фрагменты. - Основываясь на объяснении в пост- поддержке FragmentPagerAdapterholds со ссылкой на старые фрагменты, вы должны оставить создание фрагментов для
FragmentPagerAdapter
и в этом смысле у вас нет ссылки на фрагменты или их теги. Если у вас есть фрагмент-тег, вы можете легко получить ссылку на него из 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
Ответ 3
Я создал этот метод, который работает для меня, чтобы получить ссылку на текущий фрагмент.
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
То, как я это сделал, определяет Hashtable из 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.
Если кто-то находит что-то не так с этим подходом, комментарии более чем приветствуются.
Ответ 5
Вам не нужно переопределять instantiateItem
и полагаться на совместимость создания тегов фрагмента с внутренним методом makeFragmentName
. instantiateItem
- это публичный метод, поэтому вы можете (и на самом деле вам следует) вызывать его в методе onCreate
своей деятельности, чтобы получать ссылки на экземпляры ваших фрагментов и сохранять их в локальных переменных, если вам это нужно. Просто помните, чтобы окружать множество instantiateItem
вызовов с startUpdate
и finishUpdate
способами, как описано в PagerAdapter
Javadoc:
Вызов метода PagerAdapter startUpdate (ViewGroup) указывает, что содержимое ViewPager собирается измениться. Затем последует один или несколько вызовов instantiateItem (ViewGroup, int) и/или destroyItem (ViewGroup, int, Object), и об окончании обновления будет сигнализироваться вызовом finishUpdate (ViewGroup).
Так, например, это способ хранения ссылок на фрагменты ваших вкладок в методе 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
. Только если они еще не существуют, он создаст новые, используя метод getItem
из вашего адаптера, и "сохранит" их в FragmentManager
для любого будущего использования.
Некоторая дополнительная информация:
Если вы не вызываете instantiateItem
окруженный startUpdate
/finishUpdate
в своем методе onCreate
то вы рискуете, что ваши экземпляры фрагмента никогда не будут переданы FragmentManager
: когда ваша деятельность становится передним планом, instantiateItem
будет вызываться автоматически для получения ваших фрагментов, но startUpdate
/finishUpdate
может не (в зависимости от деталей реализации) и что они в основном делают, это начинают/фиксируют FragmentTransaction
.
Это может привести к тому, что ссылки на созданные экземпляры фрагментов будут очень быстро потеряны (например, при повороте экрана) и воссозданы гораздо чаще, чем это необходимо. В зависимости от того, насколько "тяжелы" ваши фрагменты, это может иметь немаловажные последствия для производительности.
Однако, что более важно, в этом случае экземпляры фрагментов, хранящихся в локальных переменных, станут устаревшими: поскольку платформа Android не смогла получить те же экземпляры из FragmentManager
она может создавать и использовать новые, в то время как ваши переменные будут по-прежнему ссылаться на старые.
Ответ 6
решение, предложенное @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 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 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;
}
}
Ответ 7
Основной дорожный блок с получением дескриптора фрагментов - вы не можете полагаться на getItem(). После изменения ориентации ссылки на фрагменты будут нулевыми, а getItem() не будет вызываться снова.
Здесь используется подход, который не полагается на реализацию FragmentPagerAdapter для получения тега. Override 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;
}
Ответ 8
Я всегда использую этот базовый класс, когда мне нужно получить доступ к дочерним фрагментам или первичным (в настоящее время видимым) фрагменту. Он не полагается на какие-либо детали реализации, и он заботится о изменениях жизненного цикла, потому что в обоих случаях вызываются перезаписываемые методы - когда создается новый экземпляр фрагмента и когда экземпляр получает от 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);
}
}
Ответ 9
Мне удалось решить эту проблему, используя идентификаторы вместо тегов. (Я использую я определил FragmentStatePagerAdapter, который использует мои пользовательские фрагменты, в которых я перепробовал метод onAttach, где вы где-то сохраняете id:
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
И тогда вы просто легко получаете доступ к фрагменту внутри действия:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
Ответ 10
См. этот пост о возврате фрагментов из FragmentPagerAdapter. Означает ли вы, что вы указали индекс своего фрагмента, но это будет установлено в getItem() (только при создании экземпляра).
Ответ 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);
}
Этот подход придает действительный видимый фрагмент активности без создания нового.
Ответ 12
Не уверен, что мой метод был правильным или лучшим способом сделать это, так как я относительный новичок в Java/Android, но он сработал (я уверен, что он нарушает объектно-ориентированные принципы, но никакое другое решение не сработало для моего варианта использования).
У меня была хостинговая активность, которая использовала ViewPager с FragmentStatePagerAdapter. Чтобы получить ссылки на фрагменты, созданные 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;
}
}