Multiple frames, loading multiple fragments, can't control backstack - android

I have an activity with two containers for fragments, container_left and container_right.
At the beginning of navigation, a list fragment is in container_left, and the fragment that loads into container_right is a screen with several buttons.
When someone selects a button in the fragment in container_right, that fragment replaces the one in container_left, and a new fragment is loaded into container_right. At this point, I have saved this transaction to the backstack. If the back button is pressed at this point, the original list loads into container_left, and the button fragment loads into container_right. But if the person selects another button (now in the left hand frame), it adds a different fragment to the right hand container. I don't want to add the new transaction to the backstack, as I don't want to save the transactions where only fragment_container_right changes. I want the back button to only change the positions of the fragments.
The problem is, the transaction in the backstack is looking for the original fragment that was removed in the right frame, and since that has changed, it doesn't remove the new fragment, so the button fragment appears ON TOP of the new fragment in container_right.
I have been messing with this for a while, but I can't figure this one out.
How can I set this up so that when it pops the stack, any fragments currently in container_right are removed, even if they aren't the ones that were there when the transaction was committed?
here is a sample of what I have so far for loading the frames. As you can see, I check to see if the control buttons are in the left frame before committing, so it isn't loading a new instance on every button push, and I do the same for each fragment loading into container_right, so I'm not creating a new fragment if someone hits the button for a fragment that is already loaded. The only issue left is the backstack transaction.
if (!(controlsInLeftFrame)) {
getFragmentManager().beginTransaction().
replace(R.id.fragment_container_right, fragAD).
replace(R.id.fragment_container_left, fragRECB).addToBackStack(null).commit();
} else if (!(getFragmentManager().findFragmentById(R.id.fragment_container_right) instanceof FragAttributeDescription)){
getFragmentManager().beginTransaction().
replace(R.id.fragment_container_right, fragAD).commit();
}
}
if (message.equals("Movement")) {
FragRaceEditorMovement fragRM = new FragRaceEditorMovement();
fragRM.setArguments(bundle);
if (!(controlsInLeftFrame)) {
getFragmentManager().beginTransaction().
replace(R.id.fragment_container_right, fragRM).
replace(R.id.fragment_container_left, fragRECB).addToBackStack(null).commit();
} else if (!(getFragmentManager().findFragmentById(R.id.fragment_container_right) instanceof FragRaceEditorMovement)){
getFragmentManager().beginTransaction().
replace(R.id.fragment_container_right, fragRM).commit();
}

Okay, I figured it out.
overriding onBackPressed() worked. I had tried it once before, but realized that I had forgotten to put super.onBackPressed() AFTER removing the right hand fragment.
public void onBackPressed() {
getFragmentManager().beginTransaction().remove(getFragmentManager().findFragmentById(R.id.fragment_container_right)).commit();
super.onBackPressed();
}
this appears to have solved the problem adequately

Related

Fragments are drawn on top of eachother

The scenario is this...I click on a button that loads the next fragment. While the animation is still going I hit the recent apps button. After the recent apps screen is shown i return to my app.
Then I see the two fragments drawn on top of each other. As if Android forgot to remove the previous fragment.
I click back to remove all fragments until I reach the first fragment loaded in this FragmentActivity. I log all the fragments I get from FragmentManager and it only shows one. But I still see two fragments drawn one on top of the other. The one that should have been removed is not responding to touch events and the other one responds as it should.
Is this an Android bug or my fault? Is there any way to fix this or prevent it from happening ?
It does not happen every time. This is how I load my fragments
public void loadNewFragment(AnimFragment newFragment, boolean addToSTack, boolean animate, String tag) {
if (newFragment != null) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.anim.fragment_enter_from_right, R.anim.fragment_exit_to_left, R.anim.fragment_enter_from_left, R.anim.fragment_exit_to_right);
transaction.replace(R.id.fragment_container, newFragment, tag);
if (addToSTack) {
transaction.addToBackStack(tag);
}
int transactionId = transaction.commit();
newFragment.setTransactionId(transactionId);
}
}
and I remove them on back pressed with
getSupportFragmentManager().popBackStackImmediate();
In my opinion "below" fragment is onPause() state and it's still alive and visible.
Try to use white background on the root layout of the front fragment to avoid showing the below fragment. Moreover the root layout size (width and height) of the front fragment should be match_parent.
Check this site to understand Fragment lifecycle.
https://google-developer-training.gitbooks.io/android-developer-advanced-course-concepts/unit-1-expand-the-user-experience/lesson-1-fragments/1-2-c-fragment-lifecycle-and-communications/1-2-c-fragment-lifecycle-and-communications.html

Switching Activities

I've been researching a solution to my problem but not a single result fixes my problem.
I have five Java activities. Each activity has five buttons, four to the other activities and one to itself. It's basically just a bar of bottoms at the bottom of the page.
My issue is that let's say I'm in one activity, and then I click that button to the activity again, it opens and loads an entirely new activity. And then when I click back, it shows the previous activity open as I left it. But if I press it ten times, I need to click back ten times. Also, if I switch between each activity twice and I want to click back to main to exit, I need to click back ten times instead of five.
I want it to be such that when I click a button, it opens the activity. If I press the button again, it does nothing if I'm in that activity. And if I'm switching between activities and I repress an activity I already opened, I want that to be brought to the top of the stack from its lower location, not added. So at most, I only want five activities running.
It's a very confusing problem as I see people suggesting to use intent flags, which I'm not sure if I place them in the manifest, which does nothing with I try. And I've seen people suggest using LaunchMode single instance, which also does nothing.
I really think fragments are the best method here. And yes, it's a little tricky but you can use fragments within fragments. For navigating without creating extra instances, I think you might be looking for something along the lines of this:
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
if(id == R.id.nav_home){
Fragment newFragment;
FragmentManager fragmentManager = getSupportFragmentManager();
// Look for fragment by tag
Fragment foundFragment = fragmentManager.findFragmentByTag(MAIN_FRAGMENT);
if(foundFragment != null) { //if fragment's already in the backstack
newFragment = foundFragment; //replace fragment with backstack fragment
}else{
newFragment = new MainFragment(); // use a new fragment instance
}
// Add new fragment instance to fragment manager
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, newFragment, MAIN_FRAGMENT)
.addToBackStack(MAIN_FRAGMENT)
.commit();
}
// ...
}

How to avoid multiple instances of fragments in Activity after app is killed and resumed?

I have an app with a Home screen that has 2 fragments (for now) and a navigation drawer. Currently I load the fragment A (Explore) on startup and load fragment B when clicked. From then on, I show and hide fragments. It's faster than recreating fragments on every click and my fragment A takes some time to load.
I've noticed that when I go to fragment B and go to another activity (let's call it activity 2) from there and leave the app and wait for it to be killed (or do something crazy like change the device language), and then come back to the same activity, it's still there. When I press back to go back to fragment B, sometimes (50% of times) the fragment B is drawn over fragment A. On clicking fragment A in the drawer, fragment A appears fine, but on clicking fragment B, there's another instance of fragment A and on top of that fragment B.
I've spent more than 2 days on this problem and got nowhere.
Here's my code for selecting the fragment:
private void selectItem(int position, boolean addExploreFragment) {
Log.d(tag, "selectItem: " + position);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
//add explore fragment - this is called on app startup, but also when the app is killed and resumed which results in 2 explore fragments
if (addExploreFragment){
fragmentTransaction.replace(R.id.content_frame, mExploreFragment, EXPLORE_FRAGMENT_TAG);
Log.d(tag, "Replaced frame and added "+ mFragmentTags[position]);
} else {
//add fragment for the first time
if (getSupportFragmentManager().findFragmentByTag(mFragmentTags[position]) == null && position != 0) {
fragmentTransaction.add(R.id.content_frame, mFragments[position], mFragmentTags[position]);
Log.d(tag, "Added Fragment: "+ mFragmentTags[position]);
}
//shows and hides fragments
for (int i = 0; i < mFragments.length; i++) {
if (i == position) {
fragmentTransaction.show(mFragments[i]);
Log.d(tag, "Showing Fragment: "+ mFragmentTags[i]);
} else {
if (getSupportFragmentManager().findFragmentByTag(mFragmentTags[i]) != null) {
fragmentTransaction.hide(mFragments[i]);
Log.d(tag, "Hid Fragment: "+ mFragmentTags[i]);
}
}
}
}
fragmentTransaction.commit();
//not null check for calling selectItem(0) before loading the drawer
if (mDrawerList != null){
mDrawerList.setItemChecked(position, true);
}
}
I know for sure, the explore fragment is getting created twice and the two instances behave independently of each other (just sharing).
I'm lost what to do next. This is an issue which can be reproduced very easily on low end devices but on a device like Nexus 4 (my test device), the issue can be reproduced by changing the device language.
Has anyone got any ideas about this? Basically if the addExploreFragment block doesn't get called when there is already an exploreFragment, this issue could be solved, I think, but I've been unable to do so. Also, I tried removing all the fragments and then adding the exploreFragment but same thing happens (50% of times).
Thanks! and sorry for the long post, I felt I should share all the details.
Update: When I change the device language and come back to the app on Activity 2 and go back to Home activity, it has the fragment B open which is good, but fragment A get recreated because it's a heavy fragment and the system probably removed it from memory. Again, that's ok that it gets recreated IF it got removed by the system but why does it get recreated when it's not removed. I believe it's something with my code, on every 2nd attempt (without closing the app) this happens, 2 instances of the heavy fragment A. Out of ideas.
But shouldn't fragmentTransaction.replace remove all the previously added fragments and then add exploreFragment. It's not working like that. Neither fragment A nor Fragment B are getting removed.
I found out something new and rather odd to me. When you use fragmentTransaction.add, the listeners you have, like DrawerItemClickListener, on the previous fragment, are still active. And this is even if you use fragmentTransaction.commit.
So...I suspect when the add method is used, you actually clicked on another hidden button or hidden UI that has an event listener on the previous fragment. I don't like this of course and the effect may be very confusing. Yes, this happened to me and I didn't understand why for a while.
For now, I think the easiest code fix would be to use the replace method instead of add. The replace() makes listeners inactive. If it works, then you can make a better/elegant fix.
Let me know what happens....
I started to notice your post
when I go to fragment B and go to another activity
When you interact or start another Activity, you start a new set of Fragments. Look at this Google webpage # Fragments Lifecycle.
For clarification of my claim, there is a quote saying
A fragment must always be embedded in an activity and the fragment's
lifecycle is directly affected by the host activity's lifecycle.
You might as well read few paragraphs of it, at least.
I am not sure what your solution should be. Perhaps make the fragments distinctive, different and clear between the two Activities you have.

Android FragmentManagerImpl.dispatchResume() resuming fragments out of order

I am hitting a very strange problem in Android and I can't figure out why it's happening or how to code around it. I truly believe this to be an Android bug.
I have a MainActivity which contains a FrameLayout named main_container (its height and width are both match_parent as each fragment should be the only fragment "showing" to the user). From MainActivity, I add Fragment A like so:
mFragmentManager.beginTransaction()
.replace(R.id.main_container, frag, fragTag)
.commit();
From there, Fragment A, upon a user's click of a view, will add Fragment B like so ("frag" and "fragTag" are different values than the above code snippet):
mFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right)
.add(R.id.main_container, frag, fragTag)
.addToBackStack(null)
.commit();
And from here, Fragment B will add Fragment C like so (again, "frag" and "fragTag" are different values than the previous two snippets):
mFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right)
.add(R.id.main_container, frag, fragTag)
.addToBackStack(null)
.commit();
So at this point, in the backstack, we should have Fragment A -> Fragment B -> Fragment C.
Fragment C invokes the MediaPicker upon the user's click of a view. Doing so calls all fragments' onPause methods and the app is put in the background. Now when the user selects an image, the application is resumed but here's where the bug happens... it resumes in this order, as proven with breakpoints in each fragments' onResume method:
Fragment A -> Fragment C -> Fragment B
This causes all sorts of issues because each of my fragments registers itself as a listener in the MainActivity to handle back button clicks. This logic relies on that ordering being correct. For some reason, it's still showing Fragment C on top, but onResume was definitely called out of order.
Perhaps even worse though... rather than clicking for MediaPicker, you can simply rotate the phone to cause a config change. This exhibits the same behavior of reordering to A -> C -> B but in this case it DOES actually show the wrong fragment on top. It SHOWS Fragment B on top.
Is it a design point that you can't rely on Android to resume fragments in the same order you added them to the backstack and I'm supposed to code around it? Or am I doing something wrong? Or is this really an Android bug? I am by far not a newbie to Android development, but this one has me stumped.
EDIT:
I've pinpointed what is going on and apparently it's by design. It seems pretty crazy to me and I disagree with the logic behind it. I may be able to fix this with reflection, but I don't like doing that. Anyways, on to the problem.
The problem is with the way FragmentManagerImpl keeps track of active fragments. It has an ArrayList to keep track of active fragments and when everything is paused (such as in my case where I'm starting an intent to get a photo from media gallery, thus it's leaving my app), upon resuming back into my app, it moves the fragments back to active in the same order they're in that ArrayList. Sounds great, eh?
Well here's my problem. When things are taken OUT of that ArrayList, they don't remove() the item, they just set it to null and then have logic to reuse that empty "slot" (line 1168 in the github link) when the next fragment comes along. In my case, the transient fragment that leaves a hole in the ArrayList is a DialogFragment. Putting it back into terms of my original report, Fragment A shows a DialogFragment... clicking a certain button in that DialogFragment brings up Fragment B. Clicking another view in Fragment B brings up Fragment C. But here's what happens to the ArrayList FragmentManagerImpl keeps track of after clicking the button in the DialogFragment:
{ FragA, null (used to be DialogFragment), FragB }
So apparently DialogFragment was moved out of active state after FragB was moved to active, thus leaving a hole. So now we click the view in FragB to bring up FragC and the ArrayList looks like so:
{ FragA, FragC (reused DialogFragment's slot), FragB }
We go off to the media picker, come back, and voila the fragments are resumed out of order with respect to how I instantiated them in the first place. This makes no sense to me and if you don't step into OS code with breakpoints, you never figure out why Android is not behaving the way you told it to. Seems like it would have been easier to just do an ArrayList.remove() of the fragment you removed, thus leaving no holes.
Like I said, I can probably get around this with reflection... but I'm leery of that because there is also this mIndex variable in all Fragments that corresponds to the index of it's slot in that ArrayList (mActive). So I'd have to be sure to keep those in sync... and now I have a dependency on knowing how the OS code works. :(
This is a known issue. Google "android fragment reordering" and you will get a whole page of links on the subject including some solutions.

How to keep only first added Fragment in back stack (fragment overlapping)?

Scenario what i'm trying to achieve:
Loading activity with two frame containers (for list of items and for details).
At the app launch time add listFragment in listFrame and some initial infoFragment in detailsFrame containers.
Navigating through list items without adding each detail transaction to back stack (want to keep only infoFragment in stack).
As soon as user hit back button (navigate back) he falls back to intial infoFragment what was added in launch time.
If sequential back navigation fallows then apps exit.
My code:
protected override void OnCreate(Bundle savedInstanceState)
{
...
var listFrag = new ListFragment();
var infoFrag = new InfoFragment();
var trans = FragmentManager.BeginTransaction();
trans.Add(Resource.Id.listFrame, listFrag);
trans.Add(Resource.Id.detailsFrame, infoFrag);
trans.Commit();
...
}
public void OnItemSelected(int id)
{
var detailsFrag = DetailFragment.NewInstance(id);
var trans = FragmentManager.BeginTransaction();
trans.Replace(Resource.Id.detailsFrame, detailsFrag);
if (FragmentManager.BackStackEntryCount == 0)
{
trans.AddToBackStack(null);
}
trans.Commit();
}
My problem:
After back button has been hit, infoFrag is overlapped with previous detailFrag! Why?
You can do this:
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack(getSupportFragmentManager().getBackStackEntryAt(0).getId(), getSupportFragmentManager().POP_BACK_STACK_INCLUSIVE);
} else {
super.onBackPressed();}
In your activity, so you to keep first fragment.
You shouldn't have, in your first fragment, the addToBackStack. But, in the rest, yes.
Very nice explanation by Budius. I read his advice and implemented similar navigation, which I would like to share with others.
Instead of replacing fragments like this:
Transaction.remove(detail1).add(detail2)
Transaction.remove(detail2).add(detail3)
Transaction.remove(detail3).add(detail4)
I added a fragment container layout in the activity layout file. It can be either LinearLayout, RelativeLayot or FrameLayout etc.. So in the activity on create I had this:
transaction.replace(R.id.HomeInputFragment, mainHomeFragment).commit();
mainHomeFragment is the fragment I want to get back to when pressing the back button, like infoFrag. Then, before EVERY NEXT transaction I put:
fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag2).addToBackStack(null).commit();
or
fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag3).addToBackStack(null).commit();
That way you don't have to keep track of which fragment is currenty showing.
The problem is that the transaction that you're backing from have two steps:
remove infoFrag
add detailsFrag (that is the first1 detail container that was added)
(we know that because the documentation This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here. )
So whenever the system is reverting that one transaction is reverting exactly those 2 steps, and it say nothing about the last detailFrag that was added to it, so it doesn't do anything with it.
There're two possible work arounds I can think on your case:
Keep a reference on your activity to the last detailsFrag used and use the BackStackChange listener to whenever the value change from 1 to 0 (you'll have to keep track of previous values) you also remove that one remaining fragment
on every click listener you'll have to popBackStackImmediatly() (to remove the previous transaction) and addToBackStack() on all transactions. On this workaround you can also use some setCustomAnimation magic to make sure it all looks nice on the screen (e.g. use a alpha animation from 0 to 0 duration 1 to avoid previous fragment appearing and disappearing again.
ps. I agree that the fragment manager/transaction should be a bit more clever to the way it handles back stack on .replace() actions, but that's the way it does it.
edit:
what is happening is like this (I'm adding numbers to the details to make it more clear).
Remember that .replace() = .remove().add()
Transaction.remove(info).add(detail1).addToBackStack(null) // 1st time
Transaction.remove(detail1).add(detail2) // 2nd time
Transaction.remove(detail2).add(detail3) // 3rd time
Transaction.remove(detail3).add(detail4) // 4th time
so now we have detail4 on the layout:
< Press back button >
System pops the back stack and find the following back entry to be reversed
remove(info).add(detail1);
so the system makes that transaction backward.
tries to remove detail1 (is not there, so it ignores)
re-add(info) // OVERLAP !!!
so the problem is that the system doesn't realise that there's a detail4 and that the transaction was .replace() that it was supposed to replace whatever is in there.
You could just override onBackPressed and commit a transaction to the initial fragment.
I'm guessing but:
You've added the transaction to replace infoFrag with 1st detailsFrag into the backstack.
But then you replace 1st detailsFrag with 2nd detailsFrag.
At this point when you click back, the fragment manager cannot cleanly replace 1st detailsFrag with infoFrag as 1st detailsFrag has already been removed and replaced.
Whether the overlapping behaviour is expected or not I don't know.
I would suggest debugging the Android core code to see what it is doing.
I'm not sure whether you can achieve without say overriding Activity::onBackPressed() and doing the pops yourself having added all transactions to the backstack.

Categories

Resources