ViewPager and fragments — what's the right way to store fragment's state?
片段似乎非常适合将UI逻辑分离为某些模块。但与
编辑
见下面的哑解决方案;-)
范围
主要活动有
问题
重新创建活动时(例如,在方向更改时),
问题
也许我使用了错误的模式,但即便是Android 3 Pro也没有太多关于它的内容。所以,请给我一两拳,并指出如何以正确的方式做到这一点。非常感谢!
码
主要活动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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又名助手
1 2 3 4 5 | public class BasePagerActivity extends FragmentActivity { BasePagerAdapter mPagerAdapter; ViewPager mPager; } |
适配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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 } } |
分段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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... */ } |
解
愚蠢的解决方案是使用putFragment将片段保存在onSaveInstanceState(主机Activity)中,并通过getFragment将它们放在onCreate中。但我仍然有一种奇怪的感觉,事情不应该像那样......请参阅下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @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(); ... } |
当
即使我们没有使用
处理片段时常用的方法是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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(简短)和ViewPager(long)的一些来源。
我想提供一个解决方案,扩展
下面是一个简单的示例,说明如何获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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(); } } } |
或者如果您更喜欢使用
注意:这不适用于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @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; } |
请注意,此方法不依赖于模仿
不要忘记,根据
此外,如果您正在使用
1 2 3 4 5 6 | 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源代码中可以看出,由
1 | String tag="android:switcher:" + viewId +":" + index; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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); |
如果要与此片段进行通信,可以从
1 | getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid +":0") |
我想为一个稍微不同的案例提供一个替代解决方案,因为我的许多搜索答案一直引导我到这个线程。
我的情况
- 我正在动态创建/添加页面并将它们滑动到ViewPager中,但是当轮换(onConfigurationChange)时,我最终得到一个新页面,因为当然再次调用OnCreate。但我想继续引用旋转之前创建的所有页面。
问题
- 我没有为我创建的每个片段提供唯一标识符,因此引用的唯一方法是以某种方式在Array中存储引用,以便在旋转/配置更改后恢复。
解决方法
- 关键概念是让Activity(显示片段)也管理对现有片段的引用数组,因为此活动可以利用onSaveInstanceState中的Bundles
1 | public class MainActivity extends FragmentActivity |
因此,在此活动中,我声明一个私人成员来跟踪打开的页面
1 | private List<Fragment> retainedPages = new ArrayList<Fragment>(); |
每次在onCreate中调用并恢复onSaveInstanceState时都会更新
1 2 3 4 5 6 | @Override protected void onSaveInstanceState(Bundle outState) { retainedPages = _adapter.exportList(); outState.putSerializable("retainedPages", (Serializable) retainedPages); super.onSaveInstanceState(outState); } |
...所以一旦它被存储,它可以被检索...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @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中的成员和方法才能使其工作,因此
1 | public class ViewPagerAdapter extends FragmentPagerAdapter |
一个相同的结构(如上面的MainActivity中所示)
1 | private List<Fragment> _pages = new ArrayList<Fragment>(); |
并且这种方法特别支持这种同步(如上面onSaveInstanceState中所使用的)
1 2 3 4 5 6 7 | public List<Fragment> exportList() { return _pages; } public void importList(List<Fragment> savedPages) { _pages = savedPages; } |
最后,在片段类中
1 | public class CustomFragment extends Fragment |
为了使所有这些工作,首先有两个变化
1 | public class CustomFragment extends Fragment implements Serializable |
然后将其添加到onCreate中,以便碎片不会被破坏
1 | setRetainInstance(true); |
我仍然处于围绕Fragments和Android生命周期的过程中,所以请注意,这种方法可能存在冗余/效率低下。但它适用于我,我希望可能对其他类似我的案例有帮助。
我的解决方案非常粗鲁但有效:作为我从保留数据动态创建的片段,我只需在调用
1 2 3 4 5 6 | @Override protected void onSaveInstanceState(Bundle outState) { outState.putInt("viewpagerpos", mViewPager.getCurrentItem() ); mSectionsPagerAdapter.removeAllfragments(); super.onSaveInstanceState(outState); } |
您无法在
这里是页面适配器中的代码:
1 2 3 4 5 6 7 8 9 10 | public void removeAllfragments() { if ( mFragmentList != null ) { for ( Fragment fragment : mFragmentList ) { mFm.beginTransaction().remove(fragment).commit(); } mFragmentList.clear(); notifyDataSetChanged(); } } |
创建片段后,我只保存当前页面并在
1 2 | if (savedInstanceState != null) mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) ); |
那是什么
可以在此处找到使用
确实,跨活动实例的视图分页器中的片段管理有点复杂,因为框架中的
如果有任何人遇到问题,他们的FragmentStatePagerAdapter没有正确恢复其片段的状态......即...... FragmentStatePagerAdapter正在创建新的片段,而不是从状态恢复它们......
确保在致电
在调用
要在方向更改后获取片段,您必须使用.getTag()。
1 | getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId +":" + positionOfItemInViewPager) |
为了更多的处理,我为我的PageAdapter编写了自己的ArrayList,以便在任何位置通过viewPagerId和FragmentClass获取片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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:
1 2 | mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages); myViewPager.setAdapter(mAdapter); |
在此之后,您可以通过使用其类来获取方向更改正确的片段:
1 2 | WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager() .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class)); |
我想出了这个简单而优雅的解决方案。它假定活动负责创建Fragments,而Adapter只是为它们提供服务。
这是适配器的代码(这里没什么奇怪的,除了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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中使用了这个代码。
1 2 3 4 5 6 7 | if (savedInstanceState!=null) { if (getSupportFragmentManager().getFragments()!=null) { for (Fragment fragment : getSupportFragmentManager().getFragments()) { mFragments.add(fragment); } } } |
当然,如果需要,您可以进一步微调此代码,例如确保片段是特定类的实例。
加:
1 | @SuppressLint("ValidFragment") |
在你上课之前。
它不起作用做这样的事情:
1 | @SuppressLint({"ValidFragment","HandlerLeak" }) |