ViewPager Fragments missing after popBackStack - android

I am having problems with fragments or maybe rather the way I want them to work.
In my activity I have a layout container (R.id.container1) to which I add a fragment (FragMain) programmatically at startup.
In the fragment I have 2 Fragments (Frag1, Frag2) and a ViewPager which loads several other fragments (FragA, FragB, FragC, FragD, FragE) via a FragmentPagerAdapter.
On a button press I replace the whole content of R.id.container1, so FragMain is replaced with another Fragment (FragSub).
Now when I press the back button, FragMain is loaded again, but the ViewPager isn't fully initialized, some Fragments are missing. From what I observed, it's always FragB and FragD missing, If I scroll to the empty views, the app crashes. The other fragments seem to be fine. What's going wrong here?
This thread suggests using getChildFragmentManager in creation of the PagerAdapter, but I am already doing that... Fragment in ViewPager not restored after popBackStack
I am accessing the fragments via e.g.
((FragA)((PagerAdapter)viewPager.getAdapter()).getItem(0));
Is this the best way?
Some Code :
MainActivity's onCreate
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pagerFragList = new Vector<Fragment>();
pagerFragList.add(Fragment.instantiate(this, FragA.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragB.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragC.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragD.class.getName()));
pagerFragList.add(Fragment.instantiate(this, FragE.class.getName()));
FragMain fragMain = (FragMain)getSupportFragmentManager().findFragmentByTag(FragMain.debugTag);
if(fragMain==null) fragMain = new FragMain();
FragmentHelper.replaceSupportFragment(this, fragMain, R.id.container1, false, FragMain);
}
Helper
public Vector<Fragment> getFragList() {
return pagerFragList;
}
FragMain's onViewCreated
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
pagerAdapter = new PagerAdapter(getChildFragmentManager(), ((MainActivity)getActivity()).getFragList());
viewPager = ((ViewPager)view.findViewById(R.id.viewPager));
viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(5);
viewPager.setCurrentItem(0, true);
}
The PagerAdapter
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 fragments.get(position);
}
#Override
public int getCount() {
return fragments.size();
}
}

Have no idea why this seems to be fixing it on my end, but try commenting out your "viewPager.setOffscreenPageLimit(5);" line.
I'm still investigating and will be back with more if I find anything else useful out.
The other thing it turns out that I had to do was override the "state" methods of a FragmentStatePagerAdapter like this:
#Override
public Parcelable saveState() {
return null;
}
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
//do nothing
}
In order to bypass whatever strange thing that was causing problems (must have had something to do with saving and restoring the states of nested fragments in a ViewPager...

There are alot of permutations involving the state of fragments and backstack transitions. For debug, it may help to isolate the event you are working with (backbutton , orientation change ... ) and to look at the following before and after your event....
state of fragment manager which includes the backstack i believe...
//debug
getFragmentManager().dump("", null,
new PrintWriter(System.out, true), null);
see this thread

Related

Screen rotation with ViewPager - Problem with FragmentManager and FragmentPagerAdapter

I'm having some problems when dealing with Fragment using ViewPager.
What I'm having:
An activity (say, MainActivity) that contains a ViewPager to display some Fragment(s). Some of them contains a callback interface, which will be called to do somethings in the MainActivity.
The MainActivity has a FragmentPagerAdapter class, which is used as the adapter of the ViewPager. And a List<Fragment> in FragmentPagerAdapter to store some Fragment that will be displayed on the ViewPager.
What I'm expecting:
First launch, the Fragment called the callback interface's methods when I hit a button in it and MainActivity did somethings inside that. It worked great.
After a screen rotation, I expected it to work the same as the first launch BUT
NullPointerException: attempt to invoke a method on a null reference object (particularly, the Fragment's interface) hit me in my face.
What I know:
- The getItem(int position) won't be called again once the Fragment is created. Instead the instantiateItem(ViewGroup container, int position) will be called.
- FragmentManager will store some Fragment in mActive.mValues
- ViewPager and fragments — what's the right way to store fragment's state? (I did reference to this and some other same topics on StackOverflow too.)
What I have tried and saw:
- Override instantiateItem(ViewGroup container, int position)
- Debugged for 1 day. I saw that when I pass getSupportFragmentManager() in MainActivity's onCreate() method to FragmentPagerAdapter's super constructor, in the first launch, it has an "address in memory", assume it was '1a1a1a1'. The mActive.mValues of FragmentManager saved some Fragment' "address in memory" which are identical to the List<Fragment> containing them (assume it was 'qwertyu'). Which meaned it was right.
But when I rotated the screen, passing the getSupportFragmentManager() again, the "address in memory" was completely different, assume '9f9f9f9'. And FragmentManager's mActive.mValues contained a different set of Fragment' "address in memory" too (assume 'abcdeff'), although the number of Fragment in it was equal to the number of Fragment that was saved on the first launch (before rotation).
I have added a Fragment to the List<Fragment> with a new "address in memory" (assume 'abababa'), has the callback interface. But when I hit the button in it, it was the Fragment that was in the FragmentManager's mActive.mValues after the rotation (with "address in memory" is 'abcdeff' as I assumed above), and that one didn't have the callback interface (due to not being set in MainActivity first). And caused the NullPointerException as mentioned above.
My questions now is:
- First of all, how to get rid of this problem!? It would be better to keep using FragmentPagerAdapter instead of another class. But I will consider using other class too.
- Second, can you explain why FragmentManager saved the Fragment instance before rotation. But after rotation, it creates a completely different Fragment instance but still uses it instead of the Fragment that was saved in the List<Fragment>?
Here is the code (I think I didn't use the instantiateItem(ViewGroup container, int position) method in the right way so it still caused the problem).
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Attach the SectionsPagerAdapter to the ViewPager
SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(pagerAdapter);
}
//
//
//Adapter class
private class SectionsPagerAdapter extends FragmentPagerAdapter {
private static final int PAGE_HOME = 0;
private int tabCount = 1;
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
//private FragmentManager fragmentManager;
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
//fragmentManager = fm;
//Default HomeFragment
HomeFragment homeFragment = new HomeFragment();
//Callback interface
homeFragment.setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
//This method will be called when a button in HomeFragment is clicked
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
fragmentList.add(homeFragment);
fragmentTitleList.add("Home");
}
#Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
#Override
public int getCount() {
return tabCount;
}
#Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
return fragmentList.get(position);
}
private void addNewCategory(String categoryName) {
CategoryFragment fragment = new CategoryFragment();
tabCount += 1;
fragmentList.add(fragment);
fragmentTitleList.add(categoryName);
notifyDataSetChanged();
}
}
}
Please help. I'm being insane for 2 days now...!
I believe android restarts the activity during orientation change thus making multiple instances of FragmentPagerAdapter and multiple set of instances of List.
I don't completely understand your question but I suspect instantiateItem doesn't do anything anyway. Doesn't the Fragment getItem(int pos) work without overriding instantiateItem()?
Oh well, right after i felt in sleep, I found the solution. It's true that I didn't use the instantiateItem() method in the right way. After debugging again, I found that the instantiateItem() method get call whenever I swipe (or choose if using TabLayout as well) to another Fragment, and even get call before getItem(int pos), no matter what it's the first launch or after rotation. Which is why I think we should set things up for the Fragment in the instantiateItem() method.
So here is how I use the instantiateItem() method now:
#Override
public Object instantiateItem(ViewGroup container, int position) {
fragmentList.set(position, (Fragment) super.instantiateItem(container, position));
Fragment fragment = fragmentList.get(position);
if (position == PAGE_HOME) {
((HomeFragment) fragment).setOnCategoryFragmentChangedListener(new HomeFragment.OnCategoryFragmentChangedListener() {
#Override
public void onAddNewCategory(String categoryName) {
addNewCategory(categoryName);
}
});
}
return fragment;
}
If anyone have a better solution, please just tell me if you don't mind. I will consider about it.

Can't find the way to save fragment states

The problem is the following, I have a navigation drawer with three fragments A, B and C. Within fragment B I have a ViewPager which in turn has 3 fragments but let's call it B-1, B-2 and B-3. I was reading a lot and trying but I can not do that when I navigate between the navigationDrawer I save the status of all the fragments are A, B, C and all of the ViewPager.
I tried with the freezeText attribute in the xml, keeping everything in a list within my viewpager (this for the 3 fragments of the viewpager, obviously) and I also tried with the savedinstancestate method but I can not get what I want, below I leave my code of the viewpager your adapter I have it in a separate class but it has no code that is not normal manual, if at least we can make it work that would be a breakthrough that I just could not achieve. Thank you
fragment that contains my viewPager:
public class CargarStocksFragment extends Fragment {
..
..
private List<Fragment> fragmentList;
..
..
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("Lista", (Serializable) fragmentList);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState!=null)
fragmentList= (List<Fragment>) savedInstanceState.getSerializable("Lista");
else
fragmentList = new ArrayList<Fragment>();
}
..
..
#Override
public Fragment getItem(int position) {
return new ArticuloFragment();
}

Fragment incorrectly recreated while rotation

I have a FragmentActivity which shows some fragments by ViewPager. Here's my code snippet,
MyFragmentActivity
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
viewPager = (ViewPager) findViewById(R.id.pager);
fetchDataAsync(this);
}
#Override
private void onDataReceived(List<String> data) {
viewPager.setAdapter(new MyFragmentPageAdapter(getChildFragmentManager()), data);
}
MyFragmentPagerAdapter
private class MyFragmentPagerAdapter extends FragmentPagerAdapter {
private Data data;
public MyFragmentPagerAdapter(FragmentManager fm, Data data)
{
super(fm);
this.data = data;
}
#Override
public Fragment getItem(int position) {
return MyFragment.newInstance(data.get(position));
}
#Override
public int getCount() {
return data.size();
}
#Override
public CharSequence getPageTitle(int position) {
return null;
}
}
The fragments are created dynamically by FragmentPagerAdapter and depend on data received.
My app crashed when screen rotation happened. With some search, I learned that "When a config change occurs the old Fragment isn't destroyed - it adds itself back to the activity when it's recreated". At that moment the data was null for recreated fragment so it's crashed at onCreateView.
Someone said FragmentStatePagerAdapter should work but not for my case. Another said reusing the fragment instead of recreating new ones, but ViewPager creates them automatically and how can I manage this. I want to permanently destroy the old fragments and not re-add them to activity, what could I do?
UPDATED
I removed constructor param from MyFragment, and passed it by setArgument
Bundle b = new Bundle();
b.putString("data", data.toString());
f.setArguments(b);
Now fragment can restore data while recreating since MyFragment could save state(data) in this way when config changes happened
You should handle the orientation change yourself.
It is covered by developer resources under configuration change, basically, this means adding to your AndroidManifest the attribute configChanges="orientation". Then when orientation changes, Activity.onConfigurationChanged will be called.
See for example Activity under Configuration Changes
Normally, this manual handle will result in your fragment being laid out, and so its size should change automatically.
Another solution, is to keep it like this : i mean, on orientation change, your
Activity is being recreated, and so is your Fragment.
So you need to ensure in your fragment that the data are available, eventually loading them if not.

Updating fragments/views in viewpager with fragmentStatePagerAdapter

Need some help with my problem of updating pages while using viewpager. I am using a simple viewpager with FragmentStatePagerAdapter. All I want to do is get access to the current fragment/view so as I can update some textviews in my fragment/view. I searched around in the forum and came to know few things
- One of the ways to handle this is by setting tag in instantiateItem() call back of the adapter and retreive the view by findViewbyTag. I could not understand how to implement this and I am also not sure if that will work for FragmentStatePagerAdapter.
- I explored other options as suggested in various forums, but cannot make them work.
My code is very much same as in a android http://developer.android.com/training/animation/screen-slide.html. Basic components are the same (Fragment activity xml with some display components including a textview, viewpager xml with just a view pager in it, a Fragment class and the main FragmentActivity class). in my FragmentActivity class I have added a pageChangelistener to my viewpager so as I can do my textview changes during onPageSelected().
Any help is is appreciated.
Adding the code for reference.
Public class myActivity extends FragmentActivity
//Variable declarations
protected void onCreate(Bundle savedInstanceState) {
ViewPager mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
View CurrView;
OnPageChangeListener pageChangelistener = new OnPageChangeListener() {
#Override
public void onPageSelected(int pageSelected) {
doTextViewChnges();//access the text view and update it based on pageSelected
---THIS IS WHERE I AM STUCK IN TRYING TO GET THE TEXTVIEW IN MY CURRENT FRAGMWNT/VIEW-------
}
mPager.setOnPageChangeListener(pageChangelistener);
}
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ScreenSlidePagerAdapter(android.support.v4.app.FragmentManager fm) {
super(fm);
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
return ScreenSlidePageFragment.create(position, <other parameters I want to pass>);
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
in your OnPageChangeListener:
OnPageChangeListener pageChangelistener = new OnPageChangeListener() {
#Override
public void onPageSelected(int pageSelected) {
ScreenSlidePageFragment currentFragment = (ScreenSlidePageFragment) mPagerAdapter.getItem(pageSelected)
doTextViewChnges();//access the text view and update it based on pageSelected
---THIS IS WHERE I AM STUCK IN TRYING TO GET THE TEXTVIEW IN MY CURRENT FRAGMWNT/VIEW-------
}
i assume you want to access your fragments from your activity and update the views.Fragments are parts of activities and their views can be accessed by them. Fragments are created dynamically in viewadapters and there is no straight forward and easy way to Tag them. You may simply access a fragment by its index number.first one is 0 second one is 1 and so on...
//access your fragment(in your case your first fragment)
//this part should be inside your doTextViewChnges()
Fragment fragment = (Fragment ) adapterViewPager
.getFragment(0);
if (fragment != null) {
//call a public method inside your fragment to update your text view
fragment .updateYourTextView();
}
Edit: inside your fragment create the following method and update your textview from there.
void updateYourTextView() {
yourTextView.setText("yourtext");
}

ViewPager's fragments seem to bet detached? [duplicate]

Fragments seem to be very nice for separation of UI logic into some modules. But along with ViewPager its lifecycle is still misty to me. So Guru thoughts are badly needed!
Edit
See dumb solution below ;-)
Scope
Main activity has a ViewPager with fragments. Those fragments could implement a little bit different logic for other (submain) activities, so the fragments' data is filled via a callback interface inside the activity. And everything works fine on first launch, but!...
Problem
When the activity gets recreated (e.g. on orientation change) so do the ViewPager's fragments. The code (you'll find below) says that every time the activity is created I try to create a new ViewPager fragments adapter the same as fragments (maybe this is the problem) but FragmentManager already has all these fragments stored somewhere (where?) and starts the recreation mechanism for those. So the recreation mechanism calls the "old" fragment's onAttach, onCreateView, etc. with my callback interface call for initiating data via the Activity's implemented method. But this method points to the newly created fragment which is created via the Activity's onCreate method.
Issue
Maybe I'm using wrong patterns but even Android 3 Pro book doesn't have much about it. So, please, give me one-two punch and point out how to do it the right way. Many thanks!
Code
Main Activity
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 aka helper
public class BasePagerActivity extends FragmentActivity {
BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}
Adapter
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
}
}
Fragment
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... */
}
Solution
The dumb solution is to save the fragments inside onSaveInstanceState (of host Activity) with putFragment and get them inside onCreate via getFragment. But I still have a strange feeling that things shouldn't work like that... See code below:
#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();
...
}
When the FragmentPagerAdapter adds a fragment to the FragmentManager, it uses a special tag based on the particular position that the fragment will be placed. FragmentPagerAdapter.getItem(int position) is only called when a fragment for that position does not exist. After rotating, Android will notice that it already created/saved a fragment for this particular position and so it simply tries to reconnect with it with FragmentManager.findFragmentByTag(), instead of creating a new one. All of this comes free when using the FragmentPagerAdapter and is why it is usual to have your fragment initialisation code inside the getItem(int) method.
Even if we were not using a FragmentPagerAdapter, it is not a good idea to create a new fragment every single time in Activity.onCreate(Bundle). As you have noticed, when a fragment is added to the FragmentManager, it will be recreated for you after rotating and there is no need to add it again. Doing so is a common cause of errors when working with fragments.
A usual approach when working with fragments is this:
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();
}
...
}
When using a FragmentPagerAdapter, we relinquish fragment management to the adapter, and do not have to perform the above steps. By default, it will only preload one Fragment in front and behind the current position (although it does not destroy them unless you are using FragmentStatePagerAdapter). This is controlled by ViewPager.setOffscreenPageLimit(int). Because of this, directly calling methods on the fragments outside of the adapter is not guaranteed to be valid, because they may not even be alive.
To cut a long story short, your solution to use putFragment to be able to get a reference afterwards is not so crazy, and not so unlike the normal way to use fragments anyway (above). It is difficult to obtain a reference otherwise because the fragment is added by the adapter, and not you personally. Just make sure that the offscreenPageLimit is high enough to load your desired fragments at all times, since you rely on it being present. This bypasses lazy loading capabilities of the ViewPager, but seems to be what you desire for your application.
Another approach is to override FragmentPageAdapter.instantiateItem(View, int) and save a reference to the fragment returned from the super call before returning it (it has the logic to find the fragment, if already present).
For a fuller picture, have a look at some of the source of FragmentPagerAdapter (short) and ViewPager (long).
I want to offer a solution that expands on antonyt's wonderful answer and mention of overriding FragmentPageAdapter.instantiateItem(View, int) to save references to created Fragments so you can do work on them later. This should also work with FragmentStatePagerAdapter; see notes for details.
Here's a simple example of how to get a reference to the Fragments returned by FragmentPagerAdapter that doesn't rely on the internal tags set on the Fragments. The key is to override instantiateItem() and save references in there instead 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();
}
}
}
or if you prefer to work with tags instead of class member variables/references to the Fragments you can also grab the tags set by FragmentPagerAdapter in the same manner:
NOTE: this doesn't apply to FragmentStatePagerAdapter since it doesn't set tags when creating its 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;
}
Note that this method does NOT rely on mimicking the internal tag set by FragmentPagerAdapter and instead uses proper APIs for retrieving them. This way even if the tag changes in future versions of the SupportLibrary you'll still be safe.
Don't forget that depending on the design of your Activity, the Fragments you're trying to work on may or may not exist yet, so you have to account for that by doing null checks before using your references.
Also, if instead you're working with FragmentStatePagerAdapter, then you don't want to keep hard references to your Fragments because you might have many of them and hard references would unnecessarily keep them in memory. Instead save the Fragment references in WeakReference variables instead of standard ones. Like this:
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...
}
I found another relatively easy solution for your question.
As you can see from the FragmentPagerAdapter source code, the fragments managed by FragmentPagerAdapter store in the FragmentManager under the tag generated using:
String tag="android:switcher:" + viewId + ":" + index;
The viewId is the container.getId(), the container is your ViewPager instance. The index is the position of the fragment. Hence you can save the object id to the 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);
If you want to communicate with this fragment, you can get if from FragmentManager, such as:
getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
I want to offer an alternate solution for perhaps a slightly different case, since many of my searches for answers kept leading me to this thread.
My case
- I'm creating/adding pages dynamically and sliding them into a ViewPager, but when rotated (onConfigurationChange) I end up with a new page because of course OnCreate is called again. But I want to keep reference to all the pages that were created prior to the rotation.
Problem
- I don't have unique identifiers for each fragment I create, so the only way to reference was to somehow store references in an Array to be restored after the rotation/configuration change.
Workaround
- The key concept was to have the Activity (which displays the Fragments) also manage the array of references to existing Fragments, since this activity can utilize Bundles in onSaveInstanceState
public class MainActivity extends FragmentActivity
So within this Activity, I declare a private member to track the open pages
private List<Fragment> retainedPages = new ArrayList<Fragment>();
This is updated everytime onSaveInstanceState is called and restored in onCreate
#Override
protected void onSaveInstanceState(Bundle outState) {
retainedPages = _adapter.exportList();
outState.putSerializable("retainedPages", (Serializable) retainedPages);
super.onSaveInstanceState(outState);
}
...so once it's stored, it can be retrieved...
#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);
}
These were the necessary changes to the main activity, and so I needed the members and methods within my FragmentPagerAdapter for this to work, so within
public class ViewPagerAdapter extends FragmentPagerAdapter
an identical construct (as shown above in MainActivity )
private List<Fragment> _pages = new ArrayList<Fragment>();
and this syncing (as used above in onSaveInstanceState) is supported specifically by the methods
public List<Fragment> exportList() {
return _pages;
}
public void importList(List<Fragment> savedPages) {
_pages = savedPages;
}
And then finally, in the fragment class
public class CustomFragment extends Fragment
in order for all this to work, there were two changes, first
public class CustomFragment extends Fragment implements Serializable
and then adding this to onCreate so Fragments aren't destroyed
setRetainInstance(true);
I'm still in the process of wrapping my head around Fragments and Android life cycle, so caveat here is there may be redundancies/inefficiencies in this method. But it works for me and I hope might be helpful for others with cases similar to mine.
My solution is very rude but works: being my fragments dynamically created from retained data, I simply remove all fragment from the PageAdapter before calling super.onSaveInstanceState() and then recreate them on activity creation:
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
mSectionsPagerAdapter.removeAllfragments();
super.onSaveInstanceState(outState);
}
You can't remove them in onDestroy(), otherwise you get this exception:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
Here the code in the page adapter:
public void removeAllfragments()
{
if ( mFragmentList != null ) {
for ( Fragment fragment : mFragmentList ) {
mFm.beginTransaction().remove(fragment).commit();
}
mFragmentList.clear();
notifyDataSetChanged();
}
}
I only save the current page and restore it in onCreate(), after the fragments have been created.
if (savedInstanceState != null)
mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );
What is that BasePagerAdapter? You should use one of the standard pager adapters -- either FragmentPagerAdapter or FragmentStatePagerAdapter, depending on whether you want Fragments that are no longer needed by the ViewPager to either be kept around (the former) or have their state saved (the latter) and re-created if needed again.
Sample code for using ViewPager can be found here
It is true that the management of fragments in a view pager across activity instances is a little complicated, because the FragmentManager in the framework takes care of saving the state and restoring any active fragments that the pager has made. All this really means is that the adapter when initializing needs to make sure it re-connects with whatever restored fragments there are. You can look at the code for FragmentPagerAdapter or FragmentStatePagerAdapter to see how this is done.
If anyone is having issues with their FragmentStatePagerAdapter not properly restoring the state of its fragments...ie...new Fragments are being created by the FragmentStatePagerAdapter instead of it restoring them from state...
Make sure you call ViewPager.setOffscreenPageLimit() BEFORE you call ViewPager.setAdapter(fragmentStatePagerAdapter)
Upon calling ViewPager.setOffscreenPageLimit()...the ViewPager will immediately look to its adapter and try to get its fragments. This could happen before the ViewPager has a chance to restore the Fragments from savedInstanceState(thus creating new Fragments that can't be re-initialized from SavedInstanceState because they're new).
I came up with this simple and elegant solution. It assumes that the activity is responsible for creating the Fragments, and the Adapter just serves them.
This is the adapter's code (nothing weird here, except for the fact that mFragments is a list of fragments maintained by the 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();
}
}
The whole problem of this thread is getting a reference of the "old" fragments, so I use this code in the Activity's onCreate.
if (savedInstanceState!=null) {
if (getSupportFragmentManager().getFragments()!=null) {
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
mFragments.add(fragment);
}
}
}
Of course you can further fine tune this code if needed, for example making sure the fragments are instances of a particular class.
To get the fragments after orientation change you have to use the .getTag().
getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)
For a bit more handling i wrote my own ArrayList for my PageAdapter to get the fragment by viewPagerId and the FragmentClass at any Position:
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;
}
}
So just create a MyPageArrayList with the fragments:
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()));
and add them to the viewPager:
mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages);
myViewPager.setAdapter(mAdapter);
after this you can get after orientation change the correct fragment by using its class:
WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
.findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));
A bit different opinion instead of storing the Fragments yourself just leave it to the FragmentManager and when you need to do something with the fragments look for them in the FragmentManager:
//make sure you have the right FragmentManager
//getSupportFragmentManager or getChildFragmentManager depending on what you are using to manage this stack of fragments
List<Fragment> fragments = fragmentManager.getFragments();
if(fragments != null) {
int count = fragments.size();
for (int x = 0; x < count; x++) {
Fragment fragment = fragments.get(x);
//check if this is the fragment we want,
//it may be some other inspection, tag etc.
if (fragment instanceof MyFragment) {
//do whatever we need to do with it
}
}
}
If you have a lot of Fragments and the cost of instanceof check may be not what you want, but it is good thing to have in mind that the FragmentManager already keeps account of Fragments.
add:
#SuppressLint("ValidFragment")
before your class.
it it doesn´t work do something like this:
#SuppressLint({ "ValidFragment", "HandlerLeak" })

Categories

Resources