Как получить существующие фрагменты при использовании FragmentPagerAdapter

У меня проблема с тем, чтобы мои фрагменты общались друг с другом через Activity, который использует FragmentPagerAdapterкак вспомогательный класс, который реализует управление вкладками и все детали подключения ViewPager с ассоциированным TabHost, Я реализовал FragmentPagerAdapter так же, как это предусмотрено примером проекта Android Support4Demos.

Главный вопрос в том, как я могу получить конкретный фрагмент из FragmentManager когда у меня нет ни идентификатора, ни тега? FragmentPagerAdapter создает фрагменты и автоматически генерирует Id и теги.

15 ответов

Решение

Краткое изложение проблемы

Примечание: в этом ответе я собираюсь ссылаться 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 так как это не устанавливает 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...
}

Я нашел ответ на свой вопрос, основанный на следующем посте: повторное использование фрагментов в фрагменте pageradapter

Несколько вещей, которые я узнал:

  1. getItem(int position) в FragmentPagerAdapter довольно вводит в заблуждение название того, что этот метод на самом деле делает. Создает новые фрагменты, а не возвращает существующие. В этом смысле метод должен быть переименован в нечто вроде createItem(int position) в Android SDK. Так что этот метод не помогает нам получать фрагменты.
  2. Основываясь на объяснении в пост- поддержке 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

Вам не нужно переопределять 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 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 сначала попытается получить ссылки на существующие экземпляры фрагмента из FragmentManager, Только если они еще не существуют, он будет создавать новые, используя getItem метод из вашего адаптера и "сохранить" их в FragmentManager для любого будущего использования.

Некоторая дополнительная информация:
Если ты не позвонишь instantiateItem окружен startUpdate / finishUpdate в вашем onCreate метод, то вы рискуете, что ваши экземпляры фрагмента никогда не будут переданы FragmentManager: когда ваша деятельность выходит на передний план instantiateItem будет вызван автоматически, чтобы получить ваши фрагменты, но startUpdate / finishUpdate может не (в зависимости от деталей реализации) и что они в основном делают, это начать / зафиксировать FragmentTransaction,
Это может привести к тому, что ссылки на созданные экземпляры фрагментов будут очень быстро потеряны (например, при повороте экрана) и воссозданы гораздо чаще, чем это необходимо. В зависимости от того, насколько "тяжелы" ваши фрагменты, это может иметь немаловажные последствия для производительности.
Более важно, однако, что в этом случае экземпляры фрагментов, хранящихся в локальных файлах, станут устаревшими: поскольку платформа Android не смогла получить те же экземпляры из FragmentManager он может создавать и использовать новые, в то время как ваши переменные будут ссылаться на старые.

То, как я это сделал, определил 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.

Если кто-то найдет что-то не так с этим подходом, комментарии приветствуются.

Я создал этот метод, который работает для меня, чтобы получить ссылку на текущий фрагмент.

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

FragmentStateAdapter для ViewPager2 ОБНОВЛЕНИЕ

FragmentStateAdapter вместо getItem(). И нет такого метода, как instantiateView().

Итак, когда хостинг / воссоздается после изменения конфигурации, createFragment()не звонят. Это означает, что фрагменты в адаптере не создаются снова, а вместо этого извлекаются их экземпляры. Так что, если вам нужно поработать с фрагментами, вы можете просто получить их из.

Создание адаптера:

      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
    }

Основным препятствием на пути получения фрагментов является то, что вы не можете полагаться на getItem(). После изменения ориентации ссылки на фрагменты будут нулевыми, и 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;
}

Решение, предложенное @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;
    }
}

Я всегда использую этот базовый класс, когда мне нужно получить доступ к дочерним фрагментам или первичному (в настоящее время видимому) фрагменту. Он не опирается на какие-либо подробности реализации и заботится об изменениях жизненного цикла, потому что перезаписанные методы вызываются в обоих случаях - когда создается новый экземпляр фрагмента и когда экземпляр получает от 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);
    }
}

Мне удалось решить эту проблему с помощью идентификаторов вместо тегов. (Я использую я определил FragmentStatePagerAdapter, который использует мои пользовательские фрагменты, в которых я переопределил метод onAttach, где вы где-то сохраняете id:

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

И тогда вы просто получаете доступ к фрагменту внутри действия:

Fragment f = getSupportFragmentManager.findFragmentById(fragId);

Смотрите этот пост о возврате фрагментов из FragmentPagerAdapter. Полагается ли вам, что вы знаете индекс вашего фрагмента - но это будет установлено в getItem() (только при создании экземпляра)

Не уверен, что мой метод был правильным или лучшим способом сделать это, так как я относительный новичок в 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.

Этот класс делает свое дело, не полагаясь на внутренние теги. Предупреждение: доступ к фрагментам должен осуществляться методом 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);
    }
}

Я не знаю, является ли это лучшим подходом, но больше ничего не помогло мне. Все остальные параметры, включая 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);
}

Этот подход прикрепляет фактический видимый фрагмент к действию, не создавая новый.

Просто попробуйте этот код,

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;
     }
}
Другие вопросы по тегам