I have a ViewPager in my app, this viewpager contains 10 same fragments with different arguments. Using FragmentStatePagerAdapter as the viewpager's adapter. FragmentStatePagerAdapter pre-make new instance when a page are selected and then destroy it. But I don't need 10 instances. 3 is enough. When user scroll to right, most left fragment can be reused, because GC are expensive. How to achieve it?
When you override the getItem function of the FragmentStatePagerAdapter, maintain a reference to each of the fragments, and instantiate them only if null:
private MyFragment myFragment0, myFragment1, myFragment2, ...;
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
if (myFragment0 == null) {
myFragment0 = new MyFragment();
}
return myFragment0;
case 1:
if (myFragment1 == null) {
myFragment1 = new MyFragment();
}
return myFragment1;
case 2:
if (myFragment2 == null) {
myFragment2 = new MyFragment();
}
return myFragment2;
...
}
}
Each of the 10 fragments will only be instantiated once, and each of the 10 fragments will only be instantiated once the user is 1 swipe away from viewing it.
Calling viewPager.setOffscreenPageLimit(10) like a different user suggested has a similar effect, but it actually results in instantiating all 10 fragments as soon as you set the adapter, which is likely to freeze the app for a short while and therefore not recommended.
As far as I know, unless your fragment instance has a ton of properties, destroying and creating the instances shouldn't be much of a problem. (Especially considering modern android devices with high power CPU and RAM)
But to answer your 'how to achieve' question...
You need a pool to manage the fragment instances. This will create, retain, and destroy (when necessary) the fragment instances.
Whenever ViewPagerAdapter.getItem() is called, get a fragment instance from the pool and return it.
If the pool has less than 3 instances, create one and return it. Otherwise, return an instance that is no longer used.
To determine an instance that is no longer used, keep track of which fragment instance represents which page, what the current page is, and which way the page is about to be viewed.
Now this is basic logic behind it, but is it really worth implementing all (especially #4).
I'm really sorry that I could not show you the actual code snippets since I've never tried to implement such way.
Hope it helps.
Related
My app has one MainActivity with three tabs (A, B, C).
Tab A shows FragmentA1. When I click a list entry in this fragment then FragmentA2 is shown (still in tab A). The same applies to the other tabs, some hierarchies go even deeper (FragmentC4).
All the switching and replacing of all the fragments is handled in MainActivity by Listeners. (Edit: I don't define my fragment in XML layouts but in the code only).
My Question is:
Should I hold references to all fragments in MainActivity or should I create them new everytime I need them?
What are the (dis)advantages? Can I reuse fragments by using Alternative 1, instead of recreating them everytime?
Alternative 1:
class MainActivity
private Fragment fgmtA1;
private Fragment fgmtA2;
private Fragment fgmtA3;
...
public onClickItemInA1(int itemId) {
fgmtA2 = new FragmentA2();
// put args
// replace
}
...
}
Alternative 2:
class MainActivity
...
public onClickItemInA1(int itemId) {
FragmentA2 fgmtA2 = new FragmentA2();
// put args
// replace
}
...
}
Alternative 3:
Maybe the best solution is a completely different approach?
Should I hold references to all fragments in MainActivity or should I
create them new everytime I need them?
It depends...
The only two reasons which i can think of are performance and keeping the state of a Fragment.
If you always create a new Fragment the GC will have a lot to do, which could cause some performance issues if you use a lot of bitmaps or huge data. You can reuse a Fragment by holding a reference to it in the Activity or getting the Fragment by tag or id using the methods FragmentManager.findFragmentByTag(String) or FragmentManager.findFragmentById(int). With them you can reuse already created Fragments, which should be done by default.
Furthermore if your Fragments hold some data, you will lose them or you cache it somewhere else to reacreate it if the Fragment is destroyed. While reusing a Fragment you can use onSavedInstanceState() to reacreate your state.
Hence, yes you should reuse a Fragment because it could cause system performance or headaches using anti-patterns to save some data.
I have the following setup which is quite common: in landscape mode I have 2 fragments - A and B. In portrait mode I have only fragment A. I tried to detect if I am in second setup mode by just a simple check:
getSupportFragmentManager().findFragmentById(R.id.frag_b) == null
This was working fine until I was in 2 fragment mode and rotating the device to 1 fragment mode - after that the manager was finding the fragment B and not returning null. I believe the fragment manager was somehow saving and loading its state from previous setup. The first question - why is this working this way and what can I do with it?
Second question - I tried to remove the fragment but was not able to do that. Here how I tried:
Fragment f = manager.findFragmentById(R.id.frag_b);
manager.beginTransaction().remove(f).commit();
f = manager.findFragmentById(R.id.frag_b); // still there
I guess remove() didn't work since it was not added using add() but rather loaded from previous state xml. Is there a way to remove a fragment from manager in this case?
P.S. The solution for me will be to have another way of detection in which mode I am. I already have this, just need to know how it works for better understanding of fragments and their behavior.
you can detect if you are on portrait or landscape with this:
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
Being new to fragments and after struggling a day I think I understood most of the logic behind fragments and their usage. The fact that fragment manager shows fragments other than the ones defined for current orientation is not a bug, it's a feature. Here are some observations summarized:
When changing configuration, the FragmentManager saves all the fragments it has currently and loads them back so they are ready in onCreate() method of container activity. This means that if you have fragment A and B in some layout and you rotate the device to a state where only A should be - you still will find B in FragmentManager. It might be not added (check with Fragment.isAdded()) or might be added to some other container, which is not visible now, but its there. This is quite useful since B saves its state (given that you did it properly in B's life cycle functions) and you don't have to take care of it on activity level. Maybe, at some point in future, you would like to dynamically add fragment B to your UI and it will have all its state saved from previous configuration.
Related to the second question above - fragments that need to be moved from container to container should not be declared in xml, they should be added dynamically. Otherwise you will not be able to change its container and will get IllegalStateException: Can't change container ID of fragment exception. Instead, you define containers in XML file and give them an ID, for example:
<RelativeLayout
android:id="#+id/fragmentContainer"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="0.7" />
and later add to it using something like
FragmentManager.beginTransaction().add(R.id.fragmentContainer, fragment);
If you need to use some fragment, first look if FragmentManager has it - if yes, then just reuse it - it will have its state saved as a bonus. To look for fragment:
private void MyFragment getMyFragment() {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (fragments != null) {
for (Fragment f : fragments) {
// an example of search criteria
if (f instanceof MyFragment) {
return (MyFragment) f;
}
}
}
return null;
}
if it is null then create a new one, otherwise you can go ahead and reuse it, if needed.
One way of reusing a fragment is putting it in another container. You will need some effort in order not to get IllegalStateException: Can't change container ID of fragment. Here is the code I used with some comments to help understand it:
private void moveFragment(MyFragment frag) {
int targetContainer = R.id.myContainerLandscape;
// first check if it is added to a correct place
if (frag.isAdded()) {
View v = frag.getView();
if (v != null) {
int id = ((ViewGroup) v.getParent()).getId();
if (id == targetContainer) {
// already added to correct container, skip
return;
}
}
}
FragmentManager manager = getSupportFragmentManager();
// Remove the fragment from its previous container first (done
// here without check if added or nor, check if needed).
// In order not to get 'Can't change container ID...' exception
// we need to assure several things:
// 1. its not hardcoded in xml - you can remove()
// fragment only if you have add()-ed before
// 2. if this fragment is sitting deep in a backstack
// then you will still get the above mentioned exception.
// If the stack is fragA-fragB-fragC <-top then you get
// exception on moving fragment A. Need to clean the back
// stack first. HOWEVER, note that in that case you will
// lose the fragB and fragC together with their states!
// For that reason save them first - I will assume there is
// only one on top of current fragment to make code simpler.
// before cleaning save the top fragment so that it is not destroyed
OtherFragment temp = getOtherFragment(); // use function 'getMyFragment()' above
// clean the backstack
manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// remove the fragment from its current container
manager.beginTransaction().remove(frag).commit();
// call executePendingTransactions() for your changes to be available
// right after this call, otherwise the previous 'commit()' just submits
// the task to main thread and it will be done somewhere in the future
manager.executePendingTransactions();
if (getOtherFragment() == null && temp != null) {
// Add the fragment we wanted to 'save' back to manager, this
// time without any relation to backstack or container. Later
// we will be able to find it using getOtherFragment() and the
// fragment manager will be able to save/load its state for us.
manager.beginTransaction().add(temp, null).commit();
manager.executePendingTransactions();
}
// now add our fragment
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(targetContainer, frag);
transaction.commit();
manager.executePendingTransactions();
}
This was the result of my first day of dealing with fragments seriously, at least my task was accomplished in my app. Would be good to get some comments from experienced guys on what is wrong here and what can be improved.
I use a ViewPager with a small and fixed number of views (just 3 or 4) in my MainActivity. If I follow the traditional way of implementing that ViewPager, I must do:
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new MyFragment0();
case 1:
return new MyFragment1();
case 2:
return new MyFragment2();
}
return null;
}
In this approach, usually only the first and second fragments are instantiated, and, once the user swipes to the second or third tab, the third fragment is instantiated and maybe the first one is destroyed. The advantage is clear: Android only keeps in memory fragments that the user is directly interacting with. In most of my apps I do this and everything is OK, but nowadays I am developing an app that needs these fragments to interact with each other. For example, when a user clicks on a button on the third fragment, some function must be triggered on the first one. When same data is typed in the second fragment, another function must be executed on the third one and so on... My problem is that all the fragments are not instantiated all the time, so it's really painful the need to check if they are or not, and assuring the proper functions will be called once the involved fragments are instantiated (when the user swipes to them).
My question is: how bad would be to instantiate the three (on four) fragments only once and keep all of them on memory, so I can assure they are all instantiated all the time:
MyFragment0 myFragment0 = new MyFragment0();
MyFragment1 myFragment1 = new MyFragment1();
MyFragment2 myFragment2 = new MyFragment2();
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return myFragment0;
case 1:
return myFragment1;
case 2:
return myFragment2;
}
return null;
}
Depending upon the complexity of your Fragments, it's not bad at all, however I'd implement it using the native Offset of the ViewPager:
mViewPager.setOffscreenPageLimit(4);
This causes the ViewPager to set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. So it will keep them in memory.
Experiment and see if it works. In any case, it's not bad to keep your fragments in memory, just make sure you're not reinstaciating them all the time unless it's needed.
Monitor your memory usage, and keep that under control are you're good to go.
Of course, if you instantiate a 20mb bitmap on each fragment… you're going to OOM the App.
On the other hand, design your app so that your Fragments might be destroyed. It can happen, and it's not under your total control. (Unless you leak memory). In the end, let Android do its job. ;)
In the Android docs, there is a FragmentStatePageAdapter that instantiates a Fragment every time getItem fires. Is this sane? I've checked, and this fires every time I swipe, which means it creates a Fragment every time? Is this correct?
#Override
public Fragment getItem(int i) {
Fragment fragment = new DemoObjectFragment();
Bundle args = new Bundle();
// Our object is just an integer :-P
args.putInt(DemoObjectFragment.ARG_OBJECT, i + 1);
fragment.setArguments(args);
return fragment;
}
I'm pretty new to Android, so I just wanted a sanity check on this. It doesn't sound right.
This is normal with FragmentStatePagerAdapter.
As per the documentation:
This version of the pager is more useful when there are a large number
of pages, working more like a list view. When pages are not visible to
the user, their entire fragment may be destroyed, only keeping the
saved state of that fragment. This allows the pager to hold on to much
less memory associated with each visited page as compared to
FragmentPagerAdapter at the cost of potentially more overhead when
switching between pages.
Thus the FragmentStatePagerAdapter does all the heavy lifting to help you keep your memory footprint relatively low. To do this, it may destroy Fragments that are not visible.
In general, you can set the number of off-screen pages for a ViewPager to keep in memory with ViewPager.setOffscreenPageLimit().
I'm getting this on some cases, in onResume(), of an activity which uses a FragmentStatePagerAdapter. When using device's back button. Not always. Not reproducible.
I'm using support package v4, last revision (8).
Already searched with google, no success finding a useful answer.
Looking in the source, it's thrown here: FragmentManager.java
#Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
if (fragment.mIndex < 0) {
throw new IllegalStateException("Fragment " + fragment
+ " is not currently in the FragmentManager");
}
bundle.putInt(key, fragment.mIndex);
}
But why is the index of fragment < 0 there?
The code instantiating the fragments:
#Override
public Fragment getItem(int position) {
Fragment fragment = null;
switch(position) {
case 0:
fragment = MyFragment.newInstance(param1);
break;
case 1:
fragment = MyFragment2.newInstance(param2, param3);
break;
}
return fragment;
}
#Override
public int getCount() {
return 2;
}
If your ViewPager is layouted inside a fragment (not an activty) :
mViewPager.setAdapter(new MyFragmentStatePagerAdapter(getChildFragmentManager()));
I had the same error here, but for another reason.
In my case I had override getItemPosition in my FragmentStatePagerAdapter. My ideia was to return the position, if the item exists, or a POSITION_NONE, if it doesn't exists anymore.
Well, my problem was the fact that when my collection got empty I returned POSITION_NONE. And that broke everything.
My fix was to return POSITION_UNCHANGED when I had an empty collection.
Hope it helps someone else.
The two key things to understand the bug are:
It happens sometimes.
It happens in onResume().
Given this information, it's likely that the ViewPager is not retaining the state of your Fragments. If you are manipulating the Fragments directly from the Activity, it could be the case that the off-page Fragment is getting destroyed and your Activity is trying to manipulate a null fragment. To retain the Fragment's state even when it is not in the current screen, the fix is pretty simple:
private static final int NUM_ITEMS = 2;
ViewPager mPager = /** instantiate viewpager **/;
mPager.setOffscreenPageLimit(NUM_ITEMS-1);
You can read about it here:
ViewPager Fragments getting destroyed over time?
Got it, the reason was, that I'm intantiating the Adapter each time in onResume().
If I instantiate the adapter only once, in the life cycle of the activity, this does not happen anymore.
This exceptions means that you are trying to attach a fragment to an activity which is no longer correct state to attach the fragments. What it means is, whenever we try to attach fragments (especially through an asynchronous call), there is a small probability that someone has pressed the back button and activity is in merge of getting destroyed while you are attaching the fragment. This is not always reproducible as its just a race condition, and might not always occur..
There are two ways to fix this:
1) This happens when you the onSaveInstanceState of your activity has been called and post to that you are trying to attach the fragment, since android wont be able to save the state of your fragment now, it will throw an exception. To overcome this and if you are not saving the state of your fragment, try using
commitAllowingStateLoss(), while committing the transaction.
2) To be very safe, check whether your activity is in correct state or not before attaching the fragment, use the following code in onPause:
boolean isInCorrectState;
public void onCreate{
super.onCreate();
isInCorrectState = true;
}
public void onPause() {
super.onPause();
if(isFinishing()){
isInCorrectState = false;
}
}
Now use this flag to check if your activity is in correct state or not before attaching the fragment.. Meaning attach the fragment iff isInCorrectState == true.
For me the reason was something else.
In my Loader.onLoaderReset() I cleared the data from the adapter. When I was leaving the app, onDestroy() caused the loader to reset, which cleared the FragmentStatePagerAdapter. I think it caused the adapter to clear all references to it's Fragments, but somehow, the FragmentManager didn't notice and threw the Exception. Doesn't seem very logical to me.
Note that for me it happened Activity.onDestroy().
Hope it helps someone.
Fragments in the ViewPager are fixed, instead of trying to replace the fragments in the adapter, try to give a different set of fragments and notifyDataSet changed, or take the advantage of FrameLayout to show another fragment over the view pager tab's current fragment.
There is my solution that works:
Swipe Gesture applied at Fragment level along with ViewPager with it's default swipe disabled