ViewPager и фрагменты-как правильно хранить состояние фрагмента?

фрагменты кажутся очень хорошими для разделения логики пользовательского интерфейса на некоторые модули. Но вместе с ViewPager его жизненный цикл все еще туманен для меня. Так что мысли гуру крайне необходимы!

редактировать

см. тупое решение ниже ; -)

область

основная деятельность имеет ViewPager С фрагментами. Эти фрагменты могут реализовать немного другую логику для других действий (submain), поэтому данные фрагментов заполняются через интерфейс обратного вызова внутри деятельности. И все отлично работает при первом запуске, но!...

10 ответов


когда FragmentPagerAdapter добавляет фрагмент в FragmentManager, он использует специальный тег, основанный на конкретной позиции, в которую будет помещен фрагмент. FragmentPagerAdapter.getItem(int position) вызывается только тогда, когда фрагмент для этой позиции не существует. После поворота Android заметит, что он уже создал/сохранил фрагмент для этой конкретной позиции, и поэтому он просто пытается подключиться к нему с помощью FragmentManager.findFragmentByTag(), вместо создания нового. Все это бесплатно при использовании FragmentPagerAdapter и почему это обычно чтобы иметь код инициализации фрагмента внутри getItem(int) метод.

даже если бы мы не использовали FragmentPagerAdapter, Не рекомендуется создавать новый фрагмент каждый раз в Activity.onCreate(Bundle). Как вы заметили, когда фрагмент добавляется в FragmentManager, он будет воссоздан для вас после поворота, и нет необходимости добавлять его снова. Это является частой причиной ошибок при работе с фрагментами.

обычный подход при работе с фрагментами это:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}

при использовании FragmentPagerAdapter, мы отказываемся от управления фрагментами адаптера и не должны выполнять вышеуказанные шаги. По умолчанию он будет предварительно загружать только один фрагмент спереди и сзади текущей позиции (хотя он не уничтожает их, если вы не используете FragmentStatePagerAdapter). Это контролируется ViewPager.setOffscreenPageLimit (int). Из-за этого прямой вызов методов на фрагментах за пределами адаптера не гарантируется, ведь они могут даже не быть в живых.

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

другой подход-переопределить FragmentPageAdapter.instantiateItem(View, int) и сохраните ссылку на фрагмент, возвращенный из супер-вызова, прежде чем возвращать его (у него есть логика, чтобы найти фрагмент, если он уже присутствует).

для более полной картины, посмотрите на некоторые из источников FragmentPagerAdapter (короткий) и ViewPager (долго).


я хочу предложить решение, которое расширяется на antonyt ' s замечательный ответ и упоминание о переопределении FragmentPageAdapter.instantiateItem(View, int) для сохранения ссылок на created Fragments так что вы можете поработать над ними позже. Это также должно работать с FragmentStatePagerAdapter; см. Примечания для деталей.


вот простой пример, как получить ссылку на элемент Fragments возвращено FragmentPagerAdapter это не зависит от внутреннего tags установить на Fragments. Ключ должен переопределить instantiateItem() и сохранить ссылки там вместо of in 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...
}

я нашел еще одно относительно простое решение для вашего вопроса.

как вы можете видеть из исходный код FragmentPagerAdapter фрагменты управляются FragmentPagerAdapter магазина в FragmentManager под тегом, созданным с помощью:

String tag="android:switcher:" + viewId + ":" + index;

на viewId - это container.getId() на container ваш ViewPager экземпляра. The index позиция фрагмента. Следовательно, вы можете сохранить идентификатор объекта в outState:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("viewpagerid" , mViewPager.getId() );
}

@Override
    protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null)
        viewpagerid=savedInstanceState.getInt("viewpagerid", -1 );  

    MyFragmentPagerAdapter titleAdapter = new MyFragmentPagerAdapter (getSupportFragmentManager() , this);        
    mViewPager = (ViewPager) findViewById(R.id.pager);
    if (viewpagerid != -1 ){
        mViewPager.setId(viewpagerid);
    }else{
        viewpagerid=mViewPager.getId();
    }
    mViewPager.setAdapter(titleAdapter);

если вы хотите общайтесь с этим фрагментом, вы можете получить если из FragmentManager, например:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")

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

мой случай - Я создаю / добавляю страницы динамически и перемещаю их в ViewPager, но при повороте (onConfigurationChange) я получаю новую страницу, потому что, конечно, OnCreate вызывается снова. Но я хочу сохранить ссылку на все страницы, которые были созданы до вращение.


мое решение очень грубо, но работает: будучи моими фрагментами, динамически созданными из сохраненных данных, я просто удаляю весь фрагмент из PageAdapter перед вызовом super.onSaveInstanceState() а затем воссоздать их при создании активности:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}

вы не можете удалить их в onDestroy(), в противном случае вы получите это исключение:

java.lang.IllegalStateException: не может выполнить это действие после onSaveInstanceState

вот код в адаптере страницы:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}

Я сохраняю только текущую страницу и восстановить его в onCreate(), после того как фрагменты были созданы.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  

что это BasePagerAdapter? Вы должны использовать один из стандартных адаптеров пейджера - либо FragmentPagerAdapter или FragmentStatePagerAdapter в зависимости от того, хотите ли вы фрагменты, которые больше не нужны ViewPager либо сохранить (первый), либо сохранить их состояние (последний) и воссоздать, если это необходимо снова.

пример кода для использования ViewPager можно найти здесь

верно, что управление фрагментами в пейджере представления между экземплярами activity является немного сложно, потому что FragmentManager в рамках заботится о сохранении состояния и восстановлении любых активных фрагментов, которые сделал пейджер. Все это действительно означает, что адаптер при инициализации должен убедиться, что он повторно соединяется с любыми восстановленными фрагментами. Вы можете посмотреть код для FragmentPagerAdapter или FragmentStatePagerAdapter чтобы увидеть, как это делается.


Если у кого-то возникают проблемы с их FragmentStatePagerAdapter неправильно восстанавливает состояние его фрагментов...то есть...новые фрагменты создаются FragmentStatePagerAdapter вместо того, чтобы восстанавливать их из состояния...

убедитесь, что вы называете ViewPager.setOffscreenPageLimit() прежде чем позвонить ViewPager.setAdapeter(fragmentStatePagerAdapter)

по вызову ViewPager.setOffscreenPageLimit()...в ViewPager будет тут посмотрите на его адаптер и попробуйте получить его фрагменты. Это может произойти до того, как ViewPager имеет возможность восстановления фрагментов из savedInstanceState (таким образом, создание новых фрагментов, которые не могут быть повторно инициализированы из SavedInstanceState, потому что они новые).


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

это код адаптера (ничего странного здесь, за исключением того, что mFragments - это список фрагментов, поддерживаемых активностью)

class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {

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

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getCount() {
        return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        TabFragment fragment = (TabFragment)mFragments.get(position);
        return fragment.getTitle();
    }
} 

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

    if (savedInstanceState!=null) {
        if (getSupportFragmentManager().getFragments()!=null) {
            for (Fragment fragment : getSupportFragmentManager().getFragments()) {
                mFragments.add(fragment);
            }
        }
    }

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


чтобы получить фрагменты после изменения ориентации нужно использовать .getTag().

    getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)

для немного большей обработки я написал свой собственный ArrayList для моего PageAdapter, чтобы получить фрагмент viewPagerId и FragmentClass в любой позиции:

public class MyPageAdapter extends FragmentPagerAdapter implements Serializable {
private final String logTAG = MyPageAdapter.class.getName() + ".";

private ArrayList<MyPageBuilder> fragmentPages;

public MyPageAdapter(FragmentManager fm, ArrayList<MyPageBuilder> fragments) {
    super(fm);
    fragmentPages = fragments;
}

@Override
public Fragment getItem(int position) {
    return this.fragmentPages.get(position).getFragment();
}

@Override
public CharSequence getPageTitle(int position) {
    return this.fragmentPages.get(position).getPageTitle();
}

@Override
public int getCount() {
    return this.fragmentPages.size();
}


public int getItemPosition(Object object) {
    //benötigt, damit bei notifyDataSetChanged alle Fragemnts refrehsed werden

    Log.d(logTAG, object.getClass().getName());
    return POSITION_NONE;
}

public Fragment getFragment(int position) {
    return getItem(position);
}

public String getTag(int position, int viewPagerId) {
    //getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.shares_detail_activity_viewpager + ":" + myViewPager.getCurrentItem())

    return "android:switcher:" + viewPagerId + ":" + position;
}

public MyPageBuilder getPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
    return new MyPageBuilder(pageTitle, icon, selectedIcon, frag);
}


public static class MyPageBuilder {

    private Fragment fragment;

    public Fragment getFragment() {
        return fragment;
    }

    public void setFragment(Fragment fragment) {
        this.fragment = fragment;
    }

    private String pageTitle;

    public String getPageTitle() {
        return pageTitle;
    }

    public void setPageTitle(String pageTitle) {
        this.pageTitle = pageTitle;
    }

    private int icon;

    public int getIconUnselected() {
        return icon;
    }

    public void setIconUnselected(int iconUnselected) {
        this.icon = iconUnselected;
    }

    private int iconSelected;

    public int getIconSelected() {
        return iconSelected;
    }

    public void setIconSelected(int iconSelected) {
        this.iconSelected = iconSelected;
    }

    public MyPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
        this.pageTitle = pageTitle;
        this.icon = icon;
        this.iconSelected = selectedIcon;
        this.fragment = frag;
    }
}

public static class MyPageArrayList extends ArrayList<MyPageBuilder> {
    private final String logTAG = MyPageArrayList.class.getName() + ".";

    public MyPageBuilder get(Class cls) {
        // Fragment über FragmentClass holen
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return super.get(indexOf(item));
            }
        }
        return null;
    }

    public String getTag(int viewPagerId, Class cls) {
        // Tag des Fragment unabhängig vom State z.B. nach bei Orientation change
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return "android:switcher:" + viewPagerId + ":" + indexOf(item);
            }
        }
        return null;
    }
}

поэтому просто создайте MyPageArrayList с фрагментами:

    myFragPages = new MyPageAdapter.MyPageArrayList();

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_data_frag),
            R.drawable.ic_sd_storage_24dp,
            R.drawable.ic_sd_storage_selected_24dp,
            new WidgetDataFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_color_frag),
            R.drawable.ic_color_24dp,
            R.drawable.ic_color_selected_24dp,
            new WidgetColorFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_textsize_frag),
            R.drawable.ic_settings_widget_24dp,
            R.drawable.ic_settings_selected_24dp,
            new WidgetTextSizeFrag()));

и добавьте их в viewPager:

    mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages);
    myViewPager.setAdapter(mAdapter);

после этого вы можете получить после изменения ориентации правильный фрагмент, используя его класс:

        WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
            .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));

добавить:

   @SuppressLint("ValidFragment")

перед вашим классом.

он не работает, сделать что-то вроде этого:

@SuppressLint({ "ValidFragment", "HandlerLeak" })