ViewPager和片段 – 存储片段状态的正确方法是什么?
片段似乎是非常好的UI界面逻辑分成一些模块。 但是与ViewPager
一起,它的生命周期对我来说还是很ViewPager
的。 所以大师的想法是非常需要的!
编辑
看到下面的愚蠢的解决scheme;-)
范围
主要活动有一个带片段的ViewPager
。 这些片段可以为其他(子域)活动实现一些不同的逻辑,所以片段的数据通过活动内的callback接口填充。 而一切正常的第一次发射,但!!
问题
当活动被重新创build(例如在方向改变时), ViewPager
的片段也是如此。 代码(你会发现下面)说,每一次创build活动,我尝试创build一个新的ViewPager
片段适配器相同的片段(也许这是问题),但FragmentManager已经有所有这些片段存储的地方(?并启动这些娱乐机制。 因此,娱乐机制调用onAttach,onCreateView等“旧”片段与我的callback接口调用启动数据通过活动的实施方法。 但是这个方法指向通过Activity的onCreate方法创build的新创build的片段。
问题
也许我正在使用错误的模式,但即使Android 3 Pro书也没有太多的关于它。 所以, 请给我一个两拳,指出如何正确的做法。 非常感谢!
码
主要活动
public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener { private MessagesFragment mMessagesFragment; @Override protected void onCreate(Bundle savedInstanceState) { Logger.d("Dash onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.viewpager_container); new DefaultToolbar(this); // create fragments to use mMessagesFragment = new MessagesFragment(); mStreamsFragment = new StreamsFragment(); // set titles and fragments for view pager Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>(); screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment()); screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment); // instantiate view pager via adapter mPager = (ViewPager) findViewById(R.id.viewpager_pager); mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager()); mPager.setAdapter(mPagerAdapter); // set title indicator TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles); indicator.setViewPager(mPager, 1); } /* set of fragments callback interface implementations */ @Override public void onMessageInitialisation() { Logger.d("Dash onMessageInitialisation"); if (mMessagesFragment != null) mMessagesFragment.loadLastMessages(); } @Override public void onMessageSelected(Message selectedMessage) { Intent intent = new Intent(this, StreamActivity.class); intent.putExtra(Message.class.getName(), selectedMessage); startActivity(intent); }
BasePagerActivity又名助手
public class BasePagerActivity extends FragmentActivity { BasePagerAdapter mPagerAdapter; ViewPager mPager; }
适配器
public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider { private Map<String, Fragment> mScreens; public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) { super(fm); this.mScreens = screenMap; } @Override public Fragment getItem(int position) { return mScreens.values().toArray(new Fragment[mScreens.size()])[position]; } @Override public int getCount() { return mScreens.size(); } @Override public String getTitle(int position) { return mScreens.keySet().toArray(new String[mScreens.size()])[position]; } // hack. we don't want to destroy our fragments and re-initiate them after @Override public void destroyItem(View container, int position, Object object) { // TODO Auto-generated method stub } }
分段
public class MessagesFragment extends ListFragment { private boolean mIsLastMessages; private List<Message> mMessagesList; private MessageArrayAdapter mAdapter; private LoadMessagesTask mLoadMessagesTask; private OnMessageListActionListener mListener; // define callback interface public interface OnMessageListActionListener { public void onMessageInitialisation(); public void onMessageSelected(Message selectedMessage); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // setting callback mListener = (OnMessageListActionListener) activity; mIsLastMessages = activity instanceof DashboardActivity; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { inflater.inflate(R.layout.fragment_listview, container); mProgressView = inflater.inflate(R.layout.listrow_progress, null); mEmptyView = inflater.inflate(R.layout.fragment_nodata, null); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // instantiate loading task mLoadMessagesTask = new LoadMessagesTask(); // instantiate list of messages mMessagesList = new ArrayList<Message>(); mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList); setListAdapter(mAdapter); } @Override public void onResume() { mListener.onMessageInitialisation(); super.onResume(); } public void onListItemClick(ListView l, View v, int position, long id) { Message selectedMessage = (Message) getListAdapter().getItem(position); mListener.onMessageSelected(selectedMessage); super.onListItemClick(l, v, position, id); } /* public methods to load messages from host acitivity, etc... */ }
解
愚蠢的解决scheme是用putFragment将片段保存在onSaveInstanceState(主机Activity)中,并通过getFragment将它们放入onCreate中。 但我仍然有一种奇怪的感觉,事情不应该这样…请参阅下面的代码:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager() .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment); } protected void onCreate(Bundle savedInstanceState) { Logger.d("Dash onCreate"); super.onCreate(savedInstanceState); ... // create fragments to use if (savedInstanceState != null) { mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment( savedInstanceState, MessagesFragment.class.getName()); StreamsFragment.class.getName()); } if (mMessagesFragment == null) mMessagesFragment = new MessagesFragment(); ... }
当FragmentPagerAdapter
将一个片段添加到FragmentManager中时,它将使用一个基于片段将被放置的特定位置的特殊标签。 FragmentPagerAdapter.getItem(int position)
只在该位置的片段不存在时被调用。 在旋转之后,Android会注意到它已经为这个特定的位置创build/保存了一个片段,所以它只是试图用FragmentManager.findFragmentByTag()
重新连接它,而不是创build一个新的FragmentManager.findFragmentByTag()
。 所有这些在使用FragmentPagerAdapter
时都是免费的,这也是为什么通常在getItem(int)
方法中使用片段初始化代码的原因。
即使我们没有使用FragmentPagerAdapter
,在Activity.onCreate(Bundle)
每次都创build一个新的片段也不是个好主意。 正如你已经注意到的那样,当一个片段被添加到FragmentManager中时,它将在旋转后被重新创build,并且不需要再次添加。 这样做是在处理碎片时出现错误的常见原因。
处理碎片时常用的方法是:
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
,我们放弃片段pipe理到适配器,不必执行上述步骤。 默认情况下,它只会在当前位置的前面和后面预加载一个片段(尽pipe除非使用FragmentStatePagerAdapter
否则不会破坏它们)。 这由ViewPager.setOffscreenPageLimit(int)控制 。 因此,直接调用适配器外部片段的方法并不保证是有效的,因为它们甚至可能不存在。
长话短说,你使用putFragment
来获得引用的解决scheme并不是那么疯狂,也不像使用片段的正常方式那样(上面)。 这是很难得到一个参考,否则因为片段是由适配器添加,而不是你个人。 只要确保offscreenPageLimit
足够高,可随时加载所需的片段,因为您依赖它存在。 这绕过了ViewPager的延迟加载function,但似乎是您对应用程序的要求。
另一种方法是重写FragmentPageAdapter.instantiateItem(View, int)
并保存对超级调用返回的片段的引用,然后返回它(如果已经存在,则有逻辑来查找片段)。
要获得更完整的图片,请查看FragmentPagerAdapter (short)和ViewPager (long)的一些源代码。
我想提供一个解决scheme,扩大antonyt
的精彩答案,并提到重写FragmentPageAdapter.instantiateItem(View, int)
保存引用创build的Fragments
所以你可以在以后的工作。 这应该也适用于FragmentStatePagerAdapter
; 请参阅说明了解详情。
下面是一个简单的例子,介绍如何获得对Fragments
返回的FragmentPagerAdapter
的引用,而不依赖FragmentPagerAdapter
上设置的内部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 (ie 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
而不是类成员variables/对Fragments
引用,那么您也可以按照相同的方式获取由FragmentPagerAdapter
设置的tags
:注意:这不适用于FragmentStatePagerAdapter
因为它在创build时不会设置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; }
请注意,此方法不依赖于模仿由FragmentPagerAdapter
设置的内部tag
,而是使用适当的API来检索它们。 这样,即使tag
在未来版本的SupportLibrary
发生变化,您仍然是安全的。
不要忘记 ,根据您的Activity
的devise,您尝试处理的Fragments
可能还可能不存在,因此您必须在使用引用之前通过执行null
检查来解决这个问题。
另外,如果你正在使用FragmentStatePagerAdapter
,那么你不想保留对你的Fragments
硬引用,因为你可能拥有很多引用,而且硬引用会不必要地将它们留在内存中。 而是将Fragment
引用保存在WeakReference
variables中而不是标准引用中。 喜欢这个:
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... }
我发现你的问题的另一个相对容易的解决scheme
正如您从FragmentPagerAdapter源代码中所看到的那样,由FragmentPagerAdapter
pipe理的FragmentPagerAdapter
存储在FragmentManager
以下标签生成的标记下:
String tag="android:switcher:" + viewId + ":" + index;
viewId
是container.getId()
, container
是你的ViewPager
实例。 index
是片段的位置。 因此,您可以将对象ID保存到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")
我想提供一个替代的解决scheme,可能是稍微不同的情况下,因为我的许多search答案不断引导我到这个线程。
我的情况 – 我创build/添加页面dynamic和滑动到一个ViewPager,但是当旋转(onConfigurationChange)我结束了一个新的页面,因为当然OnCreate再次被调用。 但是我想继续参考在旋转之前创build的所有页面。
问题 – 我没有创build每个片段的唯一标识符,所以引用的唯一方法是以某种方式将参考存储在要在循环/configuration更改后恢复的数组中。
解决方法 – 关键概念是让Activity(显示片段)也pipe理对现有片段的引用数组,因为这个活动可以利用onSaveInstanceState
public class MainActivity extends FragmentActivity
所以在这个活动中,我宣布私人会员来跟踪打开的页面
private List<Fragment> retainedPages = new ArrayList<Fragment>();
每次onSaveInstanceState被调用并在onCreate中恢复
@Override protected void onSaveInstanceState(Bundle outState) { retainedPages = _adapter.exportList(); outState.putSerializable("retainedPages", (Serializable) retainedPages); super.onSaveInstanceState(outState); }
所以一旦存储,它可以被检索…
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { retainedPages = (List<Fragment>) savedInstanceState.getSerializable("retainedPages"); } _mViewPager = (CustomViewPager) findViewById(R.id.viewPager); _adapter = new ViewPagerAdapter(getApplicationContext(), getSupportFragmentManager()); if (retainedPages.size() > 0) { _adapter.importList(retainedPages); } _mViewPager.setAdapter(_adapter); _mViewPager.setCurrentItem(_adapter.getCount()-1); }
这些是对主要活动的必要改变,所以我需要FragmentPagerAdapter中的成员和方法来工作,所以在
public class ViewPagerAdapter extends FragmentPagerAdapter
一个相同的构造(如上面的MainActivity所示)
private List<Fragment> _pages = new ArrayList<Fragment>();
和这个同步(在onSaveInstanceState中使用上面)是特别支持的方法
public List<Fragment> exportList() { return _pages; } public void importList(List<Fragment> savedPages) { _pages = savedPages; }
最后,在片段类中
public class CustomFragment extends Fragment
为了所有这些工作,首先有两个变化
public class CustomFragment extends Fragment implements Serializable
然后添加到onCreate这样碎片不被破坏
setRetainInstance(true);
我仍然在围绕着碎片和Android生命周期进行包装,所以在这里需要注意的是在这个方法中可能会出现冗余/效率低下的情况。 但它对我有用,我希望可以帮助其他类似于我的情况。
我的解决scheme是非常粗鲁的,但工作原理:作为从保留的数据dynamic创build我的片段,我只是在调用super.onSaveInstanceState()
之前从PageAdapter
删除所有片段,然后在创build活动时重新创build它们:
@Override protected void onSaveInstanceState(Bundle outState) { outState.putInt("viewpagerpos", mViewPager.getCurrentItem() ); mSectionsPagerAdapter.removeAllfragments(); super.onSaveInstanceState(outState); }
你不能在onDestroy()
删除它们,否则你会得到这个exception:
java.lang.IllegalStateException:
在onSaveInstanceState
后无法执行此操作
这里页面适配器中的代码:
public void removeAllfragments() { if ( mFragmentList != null ) { for ( Fragment fragment : mFragmentList ) { mFm.beginTransaction().remove(fragment).commit(); } mFragmentList.clear(); notifyDataSetChanged(); } }
我只保存当前页面并在onCreate()
创build片段后将其恢复。
if (savedInstanceState != null) mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );
什么是BasePagerAdapter
? 您应该使用标准的寻呼机适配器之一 – FragmentPagerAdapter
或FragmentStatePagerAdapter
,这取决于您是否希望ViewPager
不再需要的ViewPager
要么保留(前者)或保存其状态(后者),要么重新保存如果需要再次创build。
使用ViewPager
示例代码可以在这里find
确实,活动实例中视图分页器中的分段pipe理有点复杂,因为框架中的FragmentManager
负责保存状态并恢复分页器所做的任何活动分段。 所有这一切的真正含义是,初始化时适配器需要确保它与任何已恢复的碎片重新连接。 您可以查看FragmentPagerAdapter
或FragmentStatePagerAdapter
的代码,以了解如何完成此操作。
如果任何人有与他们的FragmentStatePagerAdapter没有正确地恢复其碎片状态的问题…即…新的碎片正在由FragmentStatePagerAdapter创build,而不是从状态恢复它们…
确保您在调用ViewPager.setAdapeter(fragmentStatePagerAdapter)
之前调用ViewPager.setAdapeter(fragmentStatePagerAdapter)
一旦调用ViewPager.setOffscreenPageLimit()
… ViewPager将立即查找其适配器,并尝试获取其片段。 这可能发生在ViewPager有机会从savedInstanceState中恢复碎片(从而创build新的碎片,因为它们是新的而无法从SavedInstanceState重新初始化)。
为了在方向改变后得到碎片,你必须使用.getTag()。
getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)
为了更多的处理,我为自己的PageAdapter编写了我自己的ArrayList,通过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 zB nach bei Orientation change for (MyPageBuilder item : this) { if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) { return "android:switcher:" + viewPagerId + ":" + indexOf(item); } } return null; } }
所以只要创build一个带有片段的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));
我想出了这个简单而优雅的解决scheme。 它假定活动负责创build碎片,而适配器只是为它们服务。
这是适配器的代码(这里没有什么奇怪的,除了mFragments
是由Activity维护的片段列表)
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(); } }
这个线程的整个问题是获取“旧”片段的引用,所以我在Activity的onCreate中使用这个代码。
if (savedInstanceState!=null) { if (getSupportFragmentManager().getFragments()!=null) { for (Fragment fragment : getSupportFragmentManager().getFragments()) { mFragments.add(fragment); } } }
当然,如果需要的话,你可以进一步微调这个代码,例如确保这些片段是一个特定类的实例。
加:
@SuppressLint("ValidFragment")
上课之前
它不工作做这样的事情:
@SuppressLint({ "ValidFragment", "HandlerLeak" })