поддержка FragmentPagerAdapter содержит ссылку на старые фрагменты - PullRequest
105 голосов
/ 15 марта 2012

ПОСЛЕДНЯЯ ИНФОРМАЦИЯ:

Я сузил свою проблему до проблемы, связанной с сохранением в фрагменте фрагмента фрагментов фрагмента старых фрагментов и тем, что мой просмотрщик не синхронизирован с моим FragmentManager.Смотрите этот выпуск ... http://code.google.com/p/android/issues/detail?id=19211#makechanges.Я до сих пор не знаю, как решить эту проблему.Любые предложения ...

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

List<Fragment> fragments = new Vector<Fragment>();
fragments.add(Fragment.instantiate(this, Fragment1.class.getName())); 
...
new PagerAdapter(getSupportFragmentManager(), fragments);

Реализация стандартная.Я использую ActionBarSherlock и v4 библиотеку вычислимости для фрагментов.

Моя проблема заключается в том, что после выхода из приложения, открытия нескольких других приложений и возврата фрагменты теряют свою ссылку обратно на FragmentActivity (т. Е. getActivity() == null),Я не могу понять, почему это происходит.Я пытался вручную установить setRetainInstance(true);, но это не помогает.Я полагал, что это происходит, когда моя FragmentActivity разрушается, однако это все равно происходит, если я открываю приложение до того, как получаю сообщение журнала.Есть какие-нибудь идеи?

@Override
protected void onDestroy(){
    Log.w(TAG, "DESTROYDESTROYDESTROYDESTROYDESTROYDESTROYDESTROY");
    super.onDestroy();
}

Адаптер:

public class PagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> fragments;

    public PagerAdapter(FragmentManager fm, List<Fragment> fragments) {
        super(fm);

        this.fragments = fragments;

    }

    @Override
    public Fragment getItem(int position) {

        return this.fragments.get(position);

    }

    @Override
    public int getCount() {

        return this.fragments.size();

    }

}

Один из моих фрагментов разорван, но я комментировал все, что разобрано и до сих пор не работает ...

public class MyFragment extends Fragment implements MyFragmentInterface, OnScrollListener {
...

@Override
public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    handler = new Handler();    
    setHasOptionsMenu(true);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    Log.w(TAG,"ATTACHATTACHATTACHATTACHATTACH");
    context = activity;
    if(context== null){
        Log.e("IS NULL", "NULLNULLNULLNULLNULLNULLNULLNULLNULLNULLNULL");
    }else{
        Log.d("IS NOT NULL", "NOTNOTNOTNOTNOTNOTNOTNOT");
    }

}

@Override
public void onActivityCreated(Bundle savedState) {
    super.onActivityCreated(savedState);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View v = inflater.inflate(R.layout.my_fragment,container, false);

    return v;
}


@Override
public void onResume(){
    super.onResume();
}

private void callService(){
    // do not call another service is already running
    if(startLoad || !canSet) return;
    // set flag
    startLoad = true;
    canSet = false;
    // show the bottom spinner
    addFooter();
    Intent intent = new Intent(context, MyService.class);
    intent.putExtra(MyService.STATUS_RECEIVER, resultReceiver);
    context.startService(intent);
}

private ResultReceiver resultReceiver = new ResultReceiver(null) {
    @Override
    protected void onReceiveResult(int resultCode, final Bundle resultData) {
        boolean isSet = false;
        if(resultData!=null)
        if(resultData.containsKey(MyService.STATUS_FINISHED_GET)){
            if(resultData.getBoolean(MyService.STATUS_FINISHED_GET)){
                removeFooter();
                startLoad = false;
                isSet = true;
            }
        }

        switch(resultCode){
        case MyService.STATUS_FINISHED: 
            stopSpinning();
            break;
        case SyncService.STATUS_RUNNING:
            break;
        case SyncService.STATUS_ERROR:
            break;
        }
    }
};

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    menu.clear();
    inflater.inflate(R.menu.activity, menu);
}

@Override
public void onPause(){
    super.onPause();
}

public void onScroll(AbsListView arg0, int firstVisible, int visibleCount, int totalCount) {
    boolean loadMore = /* maybe add a padding */
        firstVisible + visibleCount >= totalCount;

    boolean away = firstVisible+ visibleCount <= totalCount - visibleCount;

    if(away){
        // startLoad can now be set again
        canSet = true;
    }

    if(loadMore) 

}

public void onScrollStateChanged(AbsListView arg0, int state) {
    switch(state){
    case OnScrollListener.SCROLL_STATE_FLING: 
        adapter.setLoad(false); 
        lastState = OnScrollListener.SCROLL_STATE_FLING;
        break;
    case OnScrollListener.SCROLL_STATE_IDLE: 
        adapter.setLoad(true);
        if(lastState == SCROLL_STATE_FLING){
            // load the images on screen
        }

        lastState = OnScrollListener.SCROLL_STATE_IDLE;
        break;
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
        adapter.setLoad(true);
        if(lastState == SCROLL_STATE_FLING){
            // load the images on screen
        }

        lastState = OnScrollListener.SCROLL_STATE_TOUCH_SCROLL;
        break;
    }
}

@Override
public void onDetach(){
    super.onDetach();
    if(this.adapter!=null)
        this.adapter.clearContext();

    Log.w(TAG, "DETACHEDDETACHEDDETACHEDDETACHEDDETACHEDDETACHED");
}

public void update(final int id, String name) {
    if(name!=null){
        getActivity().getSupportActionBar().setTitle(name);
    }

}

}

Метод обновления вызывается, когда пользователь взаимодействует с другим фрагментом, а getActivity возвращает ноль.Вот метод, который вызывает другой фрагмент ...

((MyFragment) pagerAdapter.getItem(1)).update(id, name);

Я полагаю, что когда приложение уничтожается, затем создается снова, вместо того, чтобы просто запустить приложение до фрагмента по умолчанию, приложение запускается, а затем в viewpagerПереходит к последней известной странице.Это кажется странным, разве приложение не должно просто загружаться во фрагмент по умолчанию?

Ответы [ 13 ]

118 голосов
/ 17 марта 2012

Вы столкнулись с проблемой, потому что вы создаете и сохраняете ссылки на свои фрагменты за пределами PagerAdapter.getItem и пытаетесь использовать эти ссылки независимо от ViewPager. Как говорит Серафим, у вас есть гарантии, что фрагмент был создан / добавлен в ViewPager в определенное время - это следует рассматривать как деталь реализации. ViewPager выполняет ленивую загрузку своих страниц; по умолчанию загружается только текущая страница, слева и справа.

Если поместить приложение в фоновый режим, фрагменты, добавленные в диспетчер фрагментов, сохраняются автоматически. Даже если ваше приложение уничтожено, эта информация восстанавливается при повторном запуске приложения.

Теперь рассмотрим, что вы просмотрели несколько страниц, фрагменты A, B и C. Вы знаете, что они были добавлены в менеджер фрагментов. Поскольку вы используете FragmentPagerAdapter, а не FragmentStatePagerAdapter, эти фрагменты будут по-прежнему добавляться (но потенциально могут быть отключены) при переходе на другие страницы.

Считайте, что вы затем создаете фоновое приложение, а затем его убивают. Когда вы вернетесь, Android будет помнить, что вы использовали Фрагменты A, B и C в менеджере фрагментов, поэтому он воссоздает их для вас, а затем добавляет их. Однако те, которые добавлены в диспетчер фрагментов, теперь НЕ те, что есть в вашем списке фрагментов в вашей деятельности.

FragmentPagerAdapter не будет пытаться вызвать getPosition, если для этой конкретной позиции страницы уже добавлен фрагмент. Фактически, поскольку фрагмент, воссозданный Android, никогда не будет удален, у вас нет надежды заменить его вызовом getPosition. Получить указатель на него также довольно сложно, чтобы получить ссылку на него, потому что он был добавлен с неизвестным вам тегом. Это по замыслу; Вам не рекомендуется возиться с фрагментами, которыми управляет пейджер. Вы должны выполнять все свои действия внутри фрагмента, общаться с действием и запрашивать переключение на определенную страницу, если это необходимо.

Теперь вернемся к вашей проблеме с отсутствующей активностью. Вызов pagerAdapter.getItem(1)).update(id, name) после того, как все это произошло, возвращает вам фрагмент в вашем списке, , который еще не добавлен в диспетчер фрагментов , и поэтому у него не будет ссылки Activity. Я бы сказал, что ваш метод обновления должен изменить некоторую общую структуру данных (возможно, управляемую действием), а затем, когда вы перейдете на определенную страницу, он может нарисовать себя на основе этих обновленных данных.

104 голосов
/ 13 февраля 2013

Я нашел простое решение, которое сработало для меня.

Сделайте так, чтобы ваш адаптер фрагмента расширял FragmentStatePagerAdapter вместо FragmentPagerAdapter и переопределил метод onSave для возврата null

@Override
public Parcelable saveState()
{
    return null;
}

Это предотвращает воссоздание фрагмента для Android


Однажды спустя я нашел другое и лучшее решение.

Позвоните setRetainInstance(true) для всех своих фрагментов и сохраните где-нибудь ссылки на них.Я сделал это в статической переменной в своей деятельности, потому что она объявлена ​​как singleTask, и фрагменты могут оставаться одинаковыми все время.

Таким образом, Android не воссоздает фрагменты, а использует одни и те же экземпляры.

27 голосов
/ 17 марта 2012

Я решил эту проблему, получив доступ к своим фрагментам непосредственно через FragmentManager, а не через FragmentPagerAdapter, как это было.Сначала мне нужно выяснить тег фрагмента, автоматически сгенерированного FragmentPagerAdapter ...

private String getFragmentTag(int pos){
    return "android:switcher:"+R.id.viewpager+":"+pos;
}

Затем я просто получаю ссылку на этот фрагмент и делаю то, что мне нужно, так ...

Fragment f = this.getSupportFragmentManager().findFragmentByTag(getFragmentTag(1));
((MyFragmentInterface) f).update(id, name);
viewPager.setCurrentItem(1, true);

Внутри моих фрагментов я установил setRetainInstance(false);, чтобы я мог вручную добавлять значения в комплект saveInstanceState.

@Override
public void onSaveInstanceState(Bundle outState) {
    if(this.my !=null)
        outState.putInt("myId", this.my.getId());

    super.onSaveInstanceState(outState);
}

и затем в OnCreate я беру этот ключ и восстанавливаю состояние фрагмента по мере необходимости.Простое решение, которое было трудно (по крайней мере, мне) найти.

23 голосов
/ 28 января 2015

Глобальное работающее проверенное решение.

getSupportFragmentManager() несколько раз сохраняет нулевую ссылку, а пейджер View не создает новую, поскольку находит ссылку на тот же фрагмент.Таким образом, чтобы преодолеть это использование getChildFragmentManager() решает проблему простым способом.

Не делайте этого:

new PagerAdapter(getSupportFragmentManager(), fragments);

Сделайте это:

new PagerAdapter(getChildFragmentManager() , fragments);

7 голосов
/ 16 марта 2012

Не пытайтесь взаимодействовать между фрагментами в ViewPager. Вы не можете гарантировать, что другой фрагмент прикреплен или даже существует. Вместо изменения заголовка панели действий из фрагмента, вы можете сделать это из своей деятельности. Используйте стандартный шаблон интерфейса для этого:

public interface UpdateCallback
{
    void update(String name);
}

public class MyActivity extends FragmentActivity implements UpdateCallback
{
    @Override
    public void update(String name)
    {
        getSupportActionBar().setTitle(name);
    }

}

public class MyFragment extends Fragment
{
    private UpdateCallback callback;

    @Override
    public void onAttach(SupportActivity activity)
    {
        super.onAttach(activity);
        callback = (UpdateCallback) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        callback = null;
    }

    public void updateActionbar(String name)
    {
        if(callback != null)
            callback.update(name);
    }
}
4 голосов
/ 09 июня 2016

Вы можете удалить фрагменты, когда уничтожаете пейджер, в моем случае я удалил их на onDestroyView() моего фрагмента:

@Override
public void onDestroyView() {

    if (getChildFragmentManager().getFragments() != null) {
        for (Fragment fragment : getChildFragmentManager().getFragments()) {
            getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
        }
    }

    super.onDestroyView();
}
4 голосов
/ 14 ноября 2014

После нескольких часов поиска аналогичной проблемы у меня есть другое решение. По крайней мере, это сработало для меня, и мне нужно всего лишь изменить пару строк.

Это проблема, с которой я столкнулся, у меня есть операция с пейджером представления, который использует FragmentStatePagerAdapter с двумя фрагментами. Все работает нормально, пока я не заставлю активность разрушаться (опции разработчика) или пока я не поверну экран. Я сохраняю ссылку на два фрагмента после того, как они были созданы внутри метода getItem.

В этот момент действие будет создано снова, и в этот момент все работает нормально, но я потерял ссылку на свои фрагменты, поскольку getItem больше не вызывается.

Вот как я исправил эту проблему внутри FragmentStatePagerAdapter:

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

        //Update the references to the Fragments we have on the view pager
        if(position==0){
            fragTabOne = (FragOffersList)aux;
        }
        else{
            fragTabTwo = (FragOffersList) aux;
        }

        return aux;
    }

Вы не получите вызов getItem снова, если адаптер уже имеет внутреннюю ссылку на него, и вы не должны это менять. Вместо этого вы можете получить фрагмент, который он использует, посмотрев на этот другой метод instantiateItem (), который будет вызываться для каждого из ваших фрагментов.

Надеюсь, это кому-нибудь поможет.

0 голосов
/ 10 июня 2019

Поскольку люди не склонны читать комментарии, вот ответ, который в основном повторяет то, что я написал здесь :

коренная причина проблемы заключается в том, что система Android делаетне вызывать getItem для получения фрагментов, которые действительно отображаются, но instantiateItem.Этот метод сначала пытается найти и повторно использовать экземпляр фрагмента для данной вкладки в FragmentManager.Только если этот поиск завершится неудачно (что происходит только в первый раз, когда FragmentManager создается заново), тогда вызывается getItem.По понятным причинам нельзя воссоздавать фрагменты (которые могут быть тяжелыми), например, каждый раз, когда пользователь поворачивает свое устройство.
Чтобы решить эту проблему, вместо создания фрагментов с Fragment.instantiate в вашей деятельности, вы должны сделать это с помощью pagerAdapter.instantiateItem и все эти вызовы должны быть окружены startUpdate/finishUpdate вызовами метода, которые запускают / фиксируют транзакцию фрагмента соответственно.getItem должно быть местом, где фрагменты действительно создаются с использованием соответствующих конструкторов.

List<Fragment> fragments = new Vector<Fragment>();

@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);
        fragments.add(adapter.instantiateItem(viewPager, 0));
        fragments.add(adapter.instantiateItem(viewPager, 1));
        // and so on if you have more tabs...
        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
        }
}
0 голосов
/ 16 мая 2018

Точно так же, как вы знаете ...

Если добавить к списку проблем этих классов, есть довольно интересная ошибка, которой стоит поделиться.

Я использую ViewPager для навигации по дереву элементов (выберите элемент, и пейджер просмотра анимирует прокрутку вправо, и появится следующая ветвь, вернитесь назад, а ViewPager прокручивает в противоположном направлении, чтобывернуться к предыдущему узлу).

Проблема возникает, когда я толкаю фрагменты с конца FragmentStatePagerAdapter.Он достаточно умен, чтобы замечать, что элементы меняются, и достаточно умен, чтобы создавать и заменять фрагмент, когда элемент изменился.Но недостаточно умный, чтобы отбросить состояние фрагмента, или недостаточно умный, чтобы обрезать внутренне сохраненные состояния фрагмента при изменении размера адаптера.Поэтому, когда вы извлекаете элемент и вставляете новый в конец, фрагмент для нового элемента получает сохраненное состояние фрагмента для старого элемента, что вызвало абсолютный хаос в моем коде.Мои фрагменты несут данные, которые могут потребовать много работы для повторного получения из Интернета, поэтому не сохранение состояния действительно не было возможным.

У меня нет чистого обходного пути.Я использовал что-то вроде этого:

  public void onSaveInstanceState(Bundle outState) {
    IFragmentListener listener = (IFragmentListener)getActivity();
    if (listener!= null)
    {
        if (!listener.isStillInTheAdapter(this.getAdapterItem()))
        {
            return; // return empty state.
        }

    }
    super.onSaveInstanceState(outState);

    // normal saving of state for flips and 
    // paging out of the activity follows
    ....
  }

Несовершенное решение, потому что новый экземпляр фрагмента по-прежнему получает набор saveState, но, по крайней мере, он не переносит устаревшие данные.

0 голосов
/ 14 марта 2017

Я решил проблему, сохранив фрагменты в SparceArray:

public abstract class SaveFragmentsPagerAdapter extends FragmentPagerAdapter {

    SparseArray<Fragment> fragments = new SparseArray<>();

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

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        fragments.append(position, fragment);
        return fragment;
    }

    @Nullable
    public Fragment getFragmentByPosition(int position){
        return fragments.get(position);
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...