This screenshot was taken after pressing the back button after a specific set of preconditions have occurred:
- You committed a FragmentTransaction without “addToBackStack”
- You did not set the background color to the rootview of your fragments (Good boy)
- you tried to go back just after adding the above mentioned transaction.
Let me explain:
Lets assume we’ve 3 fragments: A, B, C. and you’d like to have the following behavior: User navigates in the following direction A -> B -> C; Suddenly you realize you’d like to skip “B” from the back stack letting the user jump from “C” to “A” when pressing the back button. What would you do?
I guess you’d skip the addToBackStack when performing the B->C transaction. This will avoid to save the transaction in the FM backstack. Like this?
Now, since replace is a combo of remove -> hide, the previous snippet is equivalent to:
Lets get back to our story shall we? The user is now landed on “C” and he decides to tap the back button. What happens? The FM fetches its back-stack’s first entry which is the one we saved at lines [11_16] of the previous snippet, reverses it and perform it. So the reversed transaction is equivalent to the following:
The reversed transaction tries to remove “B” from R.id.content (which is not there cause we’ve “C“) and adds “A” back. The results? We’ve both aFragment and cFragment showing on the screen! NICE 😛
You might even not see this if your fragment root view has an android:background property (possible overdraw?). But you will see a strange behavior when the user tries again to go from A to B (again) and then pressed back. Guess what? You’ll see “C” instead of A. Why?
Lets see the full navigation path to get this: “A” -> “B” -> “C” -> (back) -> [A,C] shown -> B -> (back) -> C!
The result will be C while the user might expect to get back to “A” (As it should!). This is mainly due cause of this:
When A & C overlaps they both have the same id (R.id.content) and they’re both in FragmentManager “added” list. But “C” comes before than “A” in this internal list.
Why? Lets debug the previous sentence a bit more. When the user is the first time in “C” the above mentioned list is composed by “A”, “B”, “C” (As expected), but when the user goes back (generating the A,C both showing) the transaction that is being reversed is “remove B and re-add A” hence these 2 fragments are removed by the list leaving “C” alone as first element.
Now when the user is in the “A,C” situation this happens.
Can you guess what will the result of line 11 be? Internally, the FragmentManager implementations iterates over a list and stops at the first occurrence of a fragment having such id. Now remember, “A” was added, removed. Hence the first and only occurrence here would be “C“.
When the user (now on B) taps the back button, the transaction gets reversed and since the FM thinks he removed C in favor of B, it is going to flip that removing B and in favor of C (instead of A) which is getting re-added to the FragmentManager fragments list.
Now the FragmentManager list looks like this: “A”, “C”
Can you guess what would happen if the user had a way to go from C to B with “addToBackStack” and then the user would press back again? Yeah, you’re right. “A” will be presented instead of “C“. Pretty nich uh?
So, what’s the solution?
Others have found solutions by adding some complexity on their app by checking the transactions count or find a specific transaction name to apply their own logic. Others have tried to perform a popBackstack just before adding their own new fragment (causing a glitch on the monitor)
The only solution i’ve found to work properly is to always add the transactions to the backstack and handle such “A -> B -> C (back) -> A” behavior by myself. For this reason i created a snippet that seems to work properly.
What’s the logic here?
Instead of fighting the great work of the FM when popping the back stack and restoring fragments, I thought it would be actually better to always add the transactions to the backstack (so that findFragmentById and the transaction reversing works always as expected).
Hey, adding everything on the backstack will remove any chances to go from C to A with a single back press right? Well … no!
When launching C from B I would call this static method that adds the transaction to the backstack. But if the user presses the back button, then another popBackStackImmediate is issued. If the user goes from C to another fragment I decided to maintain B in the backstack (but you might want to not do that by moving line 15 within the if statement at line 17).
Here is what the code would do, using our beloved navigation paths: A -> B -> C (user-back) -> (code-back [line:18]) -> A
If user goes to D then: A -> B -> C -> D (user-back) -> C (user-back) -> B (user-back) -> A.
Furthermore the addOnBackStackChangedListener will get auto-removed once the user either goes to D (or any other fragment) or taps on back resulting in a very low performance footprint of such solution.