In my app, I have a TopActionBar fragment that is loaded on the MainActivity that loads a MaterialToolbar, along with my navigation drawer. I have a FrameLayout in this fragment that I replace with fragments to navigate between pages. When I replace a fragment (using a function I have defined in a utils.kt file), I am tracking the fragments that are loaded for the first time and adding them to the BackStack so that I can pop them and prevent duplicates of that fragment from being added to the BackStack. Here is the relevant logic for how that is being managed in my Utils.kt file:
fun replaceFragment(destinationFragment : Fragment,
currentFragment: String,
title : String,
initialLaunch: Boolean = false
) {
val destinationFragmentName = destinationFragment.javaClass.simpleName
val fragmentTag : Fragment? = fragmentManager.findFragmentByTag(destinationFragmentName)
if(destinationFragmentName !== currentFragment || initialLaunch) {
val fragmentTransaction = fragmentManager.beginTransaction()
// some logic to determine animations depending on the fragment being replaced
if (fragmentTag == null) {
fragmentTransaction.replace(R.id.frame_layout, destinationFragment, destinationFragmentName)
fragmentTransaction.addToBackStack(destinationFragmentName)
} else { // re-use the old fragment
fragmentTransaction.replace(R.id.frame_layout, destinationFragment, destinationFragmentName)
}
fragmentTransaction.commit()
}
}
And then this is how I have overwritten the onBackPressed function in my MainActivity:
override fun onBackPressed() {
if (fragmentManager.backStackEntryCount > 0) {
fragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
}
A couple of things aren't working properly. Take this example flow of fragments below:
A -> B -> C -> B -> C
When I press back I get this flow:
BC -> AC -> App Close
Here multiple Fragments are being displayed at the same time.
What I expect to happen is:
C -> B -> A -> App Close
Can someone maybe offer some insights into why this is occurring and what I can do to fix this? If I don't conditionally addToBackStack, and just addToBackStack for every single Fragment I replace, it works fine, but I don't want the multiple copies in the BackStack. I need to keep the most recent instance of each Fragment in the BackStack. So in my example:
A -> B -> C -> B -> C
The BackStack would no longer have the first C, just the most recent one.
As per the FragmentManager guide:
When you call addToBackStack() on a transaction, note that the transaction can include any number of operations, such as adding multiple fragments, replacing fragments in multiple containers, and so on. When the back stack is popped, all of these operations are reversed as a single atomic action. If you've committed additional transactions prior to the popBackStack() call, and if you did not use addToBackStack() for the transaction, these operations are not reversed. Therefore, within a single FragmentManager, avoid interleaving transactions that affect the back stack with those that do not.
So what you are experiencing is the operation you did with addToBackStack being reversed (causing your original copy of B to reappear) while not touching the new C you did not use addToBackStack.
The FragmentManager's back stack is just that: a stack. That means you can't remove B from the stack unless it is at the top of the back stack. That means there's no way to remove B from the middle of the stack without using something like the support for multiple back stacks to completely swap between independent back stacks.
If you just want to make sure there is only one copy of B on the top of the stack, you'll want to popBackStack() to remove the topmost if the names are the same before unconditionally using addToBackStack() on your new instance.
Related
I am working on android application that contains five fragment on an activity, What I want is as the fragment 1 is opened and I back-press it comes to Main fragment and same as I press back-press from fragment 5 it also comes to Main fragment.
and When I press on Backpress from MainFragment, the App should Exit.
I have Gone through this link Link
and I have also added the Dispatcher but It not met my requirement.
Like I am always opening each fragment like this
private fun ShowQRCodeFragment() {
val newFragment: Fragment = QrCodeScanningFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack(null)
transaction1.commit()
}
Updated the transaction
private fun FunctionNewSettings() {
val newFragment: Fragment = CustomSettingsFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack("namedata")
fragmentManager.popBackStack()
transaction1.commit()
}
You should use addToBackStack() while fragment transaction. This will allow you to go to the previous fragment on back-press.
For the app exit case, check if the current fragment is MainFragment with the help of fragment tag and calling fragmentmanager.popBackStack() or super.onBackPressed() accordingly.
In MainFragment, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
requireActivity().finish()
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
In another fragments, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
for (i in 0 until (requireActivity() as FragmentActivity).supportFragmentManager.backStackEntryCount) {
activity.supportFragmentManager.popBackStack()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
if u want to go back to the previous fragment first use
addToBackStack()
and if you want to exit the app/activity by using onBackPressed from activity then in MainFragment use
getActivity().onBackPressed();
if you want to finish the activity from Fragment use
getActivity().finish();
You can also replace existing fragment when user clicks Back button using
fragmentTransaction.replace(int containerViewId, Fragment fragment, String tag)
Solution
Override onBackPressed() method inside your activity.
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count > 1) {
repeat(count - 1) { supportFragmentManager.popBackStack() }
} else {
finish()
}
}
You don't need to mess around with the back button behaviour if you're just switching fragments around, and you shouldn't need to pop the backstack either.
The backstack is just a history, like the back button on your browser. You start with some initial state, like an empty container layout. There's no history before this (nothing on the backstack), so if you hit back now, it will back out of the Activity completely.
If you start a fragment transaction where you add a new fragment to that container, you can use addToBackStack to create a new "step" in the history. So it becomes
empty container -> added fragment
and if you hit back it takes a step back (pops the most recent state off the stack)
empty container
if you don't use addToBackStack, the change replaces the current state on the top of the stack
(with addToBackStack)
empty container -> fragmentA -> fragmentB
(without it)
empty container -> fragmentB
so usually you'll skip adding to the backstack when you add your first fragment, since you don't want an earlier step with the empty container - you want to replace that state
empty container
(add mainFragment without adding the transaction to the backstack)
mainFragment
and now when you're at that first state showing mainFragment, the back button will back out of the activity
So addToBackStack makes changes that are added to the history, and you can step back through each change. Skipping it basically alters the last change instead of making a new one. You can think of it like adding to the backstack is going down a level, so when you hit back you go back up to the previous level. Skipping the add keeps you on the same level, and just changes what you're looking at - hitting back still takes you up a level.
So you can use this to organise the "path" the back button takes, by adding new steps to the stack or changing the current one. If you can write out the stack you want, where the back button takes you back a step each time, you can create it!
One last thing - addToBackStack takes a String? argument, which is usually null, but you can pass in a label for the step you're adding. This allows you to pop the backstack all the way back to a certain point in the history, which is like when a browser lets you jump to the previous site in the history, and not just the last page.
So you can add a name for the transaction, like "show subfragment" when you're adding your first subfragment on top of mainFragment, meaning you can use popBackstack with that label to jump straight to the initial mainFragment state, where the next back press exits the activity. This is way more convenient than popping each step off the backstack, and keeping track of how many you need to do - you can just jump back in the history to a defined point
I have three fragments A, B and C. And I'm using navHostFragment container in MainActivity. So the application goes from A -> B using kotlin extension function findNavController().navigate... and then go from B to C using same function. All works fine till here.
Now in Fragment C, I'm replacing different elements on fragment C using
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.list_container, someFragment)
?.addToBackStack("some_frag_id")
?.commit()
The list_container is replaced with someFragment. After this when I press physical back button Fragment C pops out and my app goes to Fragment B while what I expect it to restore replaced list_container i.e. whatever was there before replacement.
I'm also overiding this in my MainActivity
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count == 0) {
super.onBackPressed()
//additional code
}
else {
supportFragmentManager.popBackStack()
}
}
I'm not sure what is missing here. I have read a lot of solutions on stackoverflow but none worked to my satisfaction. Please guide.
If you are adding a Fragment to a View within a Fragment, you must always use the childFragmentManager - using activity?.supportFragmentManager is always the wrong FragmentManager to use in that case.
Besides fixing cases with restoring state (which would not work when using the wrong FragmentManager), this also ensures that the default behavior for dispatching onBackPressed() down the FragmentManager hierarchy will work out the box - you should not need any logic at all in onBackPressed() to have the pop work correctly.
If you need to intercept the back button in Fragment C, you should follow the providing custom back documentation to register an OnBackPressedDispatcher - you should not override onBackPressed() even in those cases.
I have 3 fragments, and I'm navigating using bottom menu (3 items), lets say I navigate this way :
A -> B -> C -> B -> C
when I press the back button, that's what will happen
A <- B <- C <- B <- C
and what I want is this
A <- B <- C
that's mean if add fragment that's already added the old one must be deleted, more precisely remove the transaction from the back stack
this code will not work because we are adding new transaction here :
FragmentTransaction transaction = mContext.beginTransaction();
Fragment lastFragment = mContext.findFragmentByTag(mFragmentTag);
if (lastFragment != null) {
transaction.remove(lastFragment);
transaction.commit();
}
btw, may some developers make a mistake, but the back stack stores transactions and NOT fragments.
To get this behaviour, you can follow something like this:
I am assuming you have a onTabSelected(int position) which gets called every time you tap on the bottom menu.
public void onTabSelected(int position, boolean wasSelected) {
FragmentManager fragmentManager = getSupportFragmentManager();
// Pop off everything up to and including the current tab
fragmentManager.popBackStack(SELECTED_FRAG_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// Add again the new tab fragment
fragmentManager.beginTransaction()
.replace(R.id.container, TabFragment.newInstance(),
String.valueOf(position)).addToBackStack(SELECTED_FRAG_TAG)).commit();
}
Firstly, you need to have tags for all your fragment. The basic idea is popBackStack upto that fragment tag that is selected.
And from the documentation of popBackStack(String name, int flags)
Pop the last fragment transition from the manager's fragment
back stack. If there is nothing to pop, false is returned.
This function is asynchronous -- it enqueues the
request to pop, but the action will not be performed until the application
returns to its event loop.
#param name If non-null, this is the name of a previous back state
to look for; if found, all states up to that state will be popped.
The {#link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
the named state itself is popped. If null, only the top state is popped.
#param flags Either 0 or {#link #POP_BACK_STACK_INCLUSIVE}.
Let's say in my app there are a few possible navigation flows (all are Fragments)
A -> B -> C -> D -> E
A -> F -> B -> C
I'd like to be able to return to fragment B regardless of the transaction backstack depth (ie. I don't want to keep track if I'm currently showing E or C). I noticed it's possible to tag the fragments, but the following code doesn't seem to work:
In fragment A create fragment B aka SocialViewFragment:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// Method 1
transaction.add(R.id.fragment_container, frag, SocialViewFragment.FRAG_TAG).commit();
// Method 2
//transaction.replace(R.id.fragment_container, frag);
//transaction.addToBackStack(SocialViewFragment.FRAG_TAG).commit();
Then in Fragment E, popBackStack returns false (and does nothing), cause it can't find the tag?!
FragmentManager mgr = PlaybackFrag.this.getActivity().getSupportFragmentManager();
if (mgr.getBackStackEntryCount() > 0) {
// Want to go back to SocialViewFragment !!!
mgr.popBackStack(SocialViewFragment.FRAG_TAG, 0); // returns False - can't find the tag!
}
It looks as though you are confusing two different types of tags.
The optional String parameter you can pass to add() is a tag for the Fragment that allows you to later find the same Fragment by calling findFragmentByTag().
The optional String parameter passed to addToBackStack() and popBackStack() is referred to as a "name" and is used to identify a particular transaction in the FragmentManager's back stack. It is not a Fragment tag because a back stack entry represents a particular transaction that could have multiple Fragment additions or removals.
To utilize the back stack names correctly, make sure you call addToBackStack() with a non-null String, then later you can call popBackStack() with the same String to pop to that particular transaction.
Also note that in your add() call you aren't calling addToBackStack() at all. Because of this, mgr.getBackStackEntryCount() will be 0 and your popBackStack() call will never happen at all (unless you have added other Fragments to the back stack).
In order to popBackStack you should addToBackStack first.
Use the method that is commented in your code:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, frag);
transaction.addToBackStack(SocialViewFragment.FRAG_TAG).commit();
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.