In portrait mode, my ViewPager has 3 fragments A, B, C but in landscape mode, it has only 2 fragments A and C. So I create 2 FragmentStatePagerAdapters for each mode. The problem is when screen orientation changed, ViewPager restores and uses previous fragments of old orientation. For example, when change orientation from portrait to landscape, ViewPager now shows 2 fragments A, B instead of A and C. I know why this happen but can't find a good solution for this.
My current workaround is to use different ids for ViewPager (eg: id/viewpager_portrait for portrait and id/viewpager_landscape for landscape layout) to prevent from reusing fragments but this cause me a memory leak because old fragment will not be destroyed and still be kept in memory.
I have tried some workaround like call super.onCreate(null) in activity's onCreate, or remove fragments of ViewPager in activity's onSaveInstanceState but they all makes my app crash.
So my question is how to avoid reusing one or many fragments in FragmentStatePagerAdapter when orientation changed?
Any helps will be appreciated. Thank in advance.
The issue probably is that the built-in PagerAdapter implementations for Fragments provided by Android assume that the items will remain constant, and so retain and reuse index-based references to all Fragments that are added to the ViewPager. These references are maintained through the FragmentManager even after the Activity (and Fragments) is recreated due to configuration changes or the process being killed.
What you need to do is to write your own implementation of PagerAdapter that associates a custom tag with each Fragment and stores the Fragments in a tag-based (instead of index-based) format. You could derive a generic implementation of this from one of the existing ones after adding an abstract method for providing a tag based on the index alongside the getItem() method. Of course, you will have to remove orphaned/unused Fragments added in the previous configuration from the ViewPager (while ideally holding on to it's state).
If you don't want to implement the whole solution yourself, then the ArrayPagerAdapter in the CWAC-Pager library can be used to provide a reasonable implementation of this with little effort. Upon initialization, you can detach the relevant Fragment based on it's provided tag, and remove/add it from the adapter as well, as appropriate.
Override getItemPosition() in your Adapter and return POSITION_NONE. So when the ViewPager is recreated it will call getItemPosition() and since you've returned POSITION_NONE from here, it will call getItem(). You should return the new fragments in from this getItem(). Ref: http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html#getItemPosition%28java.lang.Object%29
Why use two different ids for your viewpager, when you can just remove Fragment B when your orientation changes?
You can retrieve your Fragments inside onCreateView() or onResume() like this (this example works inside a parent fragment, but is also usable inside a parent activity like in onResume() ):
#Override
public void onResume() {
super.onResume();
// ... initialize components, etc.
pager.setAdapter(pagerAdapter);
List<Fragment> children = getChildFragmentManager().getFragments();
if (children != null) {
pagerAdapter.restoreFragments(children, orientation);
}
}
Then inside your adapter:
#Override
public void restoreFragments(List<Fragment> fragments, int orientation) {
List<Fragment> fragmentsToAdd = new ArrayList<Fragment>();
Collections.fill(fragmentsToAdd, null);
if (Configuration.ORIENTATION_LANDSCAPE == orientation) {
for (Fragment f : fragments) {
if (!(f instanceof FragmentB)) {
fragmentsToAdd.add(f);
}
}
}
this.fragmentsInAdapter = Arrays.copyOf(temp.toArray(new Fragment[0]), this.fragments.length); // array of all your fragments in your adapter (always refresh them, when config changes, else you have old references in your array!
notifyDataSetChanged(); // notify, to remove FragmentB
}
That should work.
Btw. if you're using Support Library V13, you can't use FragmentManager.getFragments(), thus you'll need to get them by id or tag.
Override OnConfigurationChanged() in your Activity (also add android:configChanges="orientation" to your activity in Manifest) this way you manage manually the the orientation change. Now "all" you have to do is to change the adapter in OnOrientaionChanged (also keep track of the current position). This way you use a single layout, a single ViewPager, and you don't have to worry about the fragment not getting recycled (well, you'll have plenty of work to make up for that).
Good luck!
Related
I am adding and removing Views to/from my Activity dynamically. Each of these Views is assigned an id and acts as a container for a particular Fragment. I add a Fragment to each one of these Views with conditional logic as follows:
if (supportFragmentManager.findFragmentById(R.id.someView) == null) {
supportFragmentManager.beginTransaction()
.add(R.id.someView, SomeFragment())
.commit()
}
This conditional logic ensures that a given View only has a Fragment added to it once during the lifetime of the Activity.
This logic works fine except when the Activity is recreated (due to a configuration change for example). When the Activity is recreated, the Views are not automatically recreated but the Fragments appear to survive the recreation. (I see that the Fragments have survived the recreation because the supportFragmentManager.findFragmentById(id:) calls return a non-null Fragment.)
I find that if I re-add Views to my Activity in the Activity.onCreate(savedInstanceState:) method, then the retained Fragments re-attach fine to the Views and everything is fine. However, if I delay adding the Views to a later point in the Activity lifecycle, then the Fragments do not re-attach to the Views (and the Views show up as blank).
Ultimately, this leads to confusing logic in my Activity.onCreate(savedInstanceState:) method when savedInstanceState is non-null to work around this. Either I have to re-add Views as they were at the point when the Activity was destroyed (I would prefer to do this elsewhere in the Activity) or I have to call FragmentTransaction.remove(fragment:) to remove each Fragment which survived the recreation.
Is there a way to add a Fragment to an Activity such that the Fragment does not survive Activity recreation? I see in the deprecation notice for the Fragment.setRetainInstance(retain:) method that the guidance is: "Instead of retaining the Fragment itself, use a non-retained Fragment and keep retained state in a ViewModel attached to that Fragment." However, this guidance does not give any instruction on how to define a non-retained Fragment.
There are a couple of dimensions to this answer.
Firstly, I could not find any documentation or any methods in the FragmentManager or FragmentTransaction classes which offer a means of creating a non-retained Fragment. The documentation in the deprecated Fragment.setRetainInstance(retain:) method says to use a "non-retained Fragment" but I could not find anywhere that explains what this means.
Secondly, the workaround for this problem is to remove the retained Fragment in the containing Activity's onCreate(savedInstanceState:) method so that the problematic Fragment can be recreated and attached to its containing view in a later lifecycle method, as follows:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.some_activity)
supportFragmentManager.findFragmentById(R.id.someView)?.let {
supportFragmentManager.beginTransaction().remove(it).commit()
}
}
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 admit I am still struggling with Fragments and currently I don't know where to load what:
At first I loaded my Fragment in OnCreate() of my Activity, but then I had difficulty to access it (via findViewById) and moved it to onStart(), but I can't still find the Views within the Fragments on onStart()...
So in onCreate:
setContentView(R.layout.myfrag);
FrameLayout fl = (FrameLayout)findViewById(R.id.iPortraitView); // different for Landscape
if(fl.getChildCount() == 0) {
getFragmentManager().beginTransaction()
.add(R.id.iTestView, new AFragmentClass).commit();
}
else {
getFragmentManager().beginTransaction()
.replace(R.id.iTestView, new AFragmentClass).commit();
}
Log.d("myTest", String.valueOf(fl.getChildCount()));
Now the problem is that fl.getChildCount() always returns 0 in onCreate, thereby adding new Fragments, while actually there is something in there and when I call the same code (fl.getchildCount() ) in onCreate, I get the correct count (which obviously increases each time I change the orientation). And as a result I have overlapping Views from both my landscape and portrait layouts.
I'm a a loss here and guess I'm struggling with figuring out what to load when (and especially when I can access them). Furthermore, do I have to implement all Listeners for potential Views (like Buttons contained in the various Fragments) that I might load dynamically in my Fragments?
The Fragment's onCreateView() hasn't been called at that point in the Activity-Fragment life cycle. onCreate(Bundle bundle) is called before onCreateView(), so there would be no views to get the count of at that point of execution. See the lifecycle: http://developer.android.com/guide/components/fragments.html.
In order to set listeners, and other view modifications on your fragment, you need to overright the onCreateView() method within your fragment.
I'm interested in the best way to have a single activity that switches between two fragments.
I've read probably 15 Stack Overflow posts and 5 blogs posts on how to do this, and, while I think I cobbled together a solution, I'm not convinced it's the best one. So, I want to hear people's opinions on the right way to handle this, especially with regards to the lifecycle of the parent activity and the fragments.
Here is the situation in detail:
A parent activity that can display one of two possible fragments.
The two fragments have state that I would like to persist across a session, but does not necessarily need to be persisted between sessions.
A number of other activities, such that the parent activity and the fragments could get buried in the back stack and destroyed due to low memory.
I want the ability to use the back button to move between the fragments (So as I understand it, I can't use setRetainInstance).
In addition to general architecture advice, I have the following outstanding questions:
If the parent activity is destroyed due to low memory, how do I guarantee that the states of both fragments will be retained, as per this post: When a Fragment is replaced and put in the back stack (or removed) does it stay in memory?. Do I just need a pointer to each fragment in the parent activity?
What is the best way for the parent activity to keep track of which fragment it is currently displaying?
Thanks in advance!
I ended up adding both of the fragments using the support fragment manager and then using detach/attach to switch between them. I was able to use commitAllowingStateLoss() because I retain the state of the view elsewhere, and manually set the correct fragment in onResume().
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.my_layout, new AFragment(), TAG_A);
fragmentTransaction.add(R.id.my_layout, new BFragment(), TAG_B);
fragmentTransaction.commit();
}
public void onResume() {
super.onResume();
if (this.shouldShowA) {
switchToA();
} else {
switchToB();
}
}
private void switchToA() {
AFragment fragA = (AFragment) getSupportFragmentManager().findFragmentByTag(TAG_A);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.detach(getSupportFragmentManager().findFragmentByTag(TAG_B));
fragmentTransaction.attach(fragA);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
}
You might want to consider using a ViewPager in your parent Activity so you can switch between the Fragments.
So you would be able to swipe through them.
if you want to persist their state during a session even if the parent activity is destroyed, you need to make them Parcelable, so you can save the state even if the class isn't instantiated at that time. You also need to do this if your rotating your device and want to keep the current situation/data on the Screen.
You should write them to a Parcelable in their onPause methods and recreate them from it in the onResume one. This way it doesn't matter if they are destroyed or have to be recreated due to changes in the devices orientation.
if you want to be able to switch between those fragments with the Backbutton, you can catch the buttonClick for onBackPressed and handle it accordingly.
If you need to figure out what Fragment your displaying at a given time you ask your ViewPager what Fragment he is displaying at that time, so you don't have to keep track, you can just ask someone who knows, if you need to know it.
I initially create my fragments inside the Activity onCreate(). Than I go about creating the ViewPager and setting up the adapter. I keep a global reference to the fragments so I can update them as needed. Also, these fragments are accessed by the adapter.
My issue is that I notice once the screen is rotated the Fragments are recreated but the ViewPager still contains the original fragments created...??
How am I supposed to handle the life-cycle of my fragment? I need to be able to call directly to the fragment from the activity. Is a singleton for a fragment a good idea? Or just a memory leaker?
protected void onCreate (Bundle savedInstanceState)
{
...
...
// set up cards
mFrag1 = new Frag1();
mFrag1.setOnActionEventListener(mOnActionEvents);
mFrag2 = new Frag2();
mFrag3 = new Frag3();
mFragPager = (ViewPager) findViewById(R.id.vpPager);
mFragAdapter = new FragAdapter(getSupportFragmentManager());
mFragPager.setAdapter(mCardAdapter);
mFragPager.setOnPageChangeListener(mOnCardChange);
}
Global instances and static fragments are definitely a bad idea. Unless you call setRetainInstance() on a fragment, it will be serialized to a Bundle and recreated when an the parent activity is re-created (on screen rotate, etc.). That will, of course, produce new fragment instances, and your old references will be invalid. You should get references to fragments via FragmentManager.findFragmentById/Tag() as needed and definitely not store those in static variables.
You may need to show more of our code, but as long as you create the fragments in onCreate() the references should be valid for the lifetime of the activity. Check the compatibility library sample code for more on using fragments with ViewPager.