I have Fragments X, A, B, and i'm using Navigation architecture component to navigate between them.
Fragments A, B specific, but Fragment X can be any(C,D,...);
Fragments A and B from Bottom Navigation and their "navigations icons" always on the screen, it means user can go to A or B anytime from any Fragment(include A and B):
X -> A -> B
X -> B -> A
A -> B -> X
A -> B -> A
//another ways
My problem about this case:
X -> A -> B -> A -> B -> ?
If user started from X, reached ? and begin to go back by "back" button, he goes throw A,B several times:
User pressed back:
? -> B -> A -> B -> A -> X
But I want "to exclude" fragments from backstack if they already on it:
? -> A -> B -> X
If user navigate:
X -> A -> B -> A
I want to see:
A -> B -> X
Not:
A -> B -> A -> X
I'm trying to do it with Pop To, but it can return me on one one concrete Fragment only. I need to return on my started X Fragment, not hardcoded. Inclusive and Single top is not about it.
I'm not sure i can do it with basic Navigation component, so i need your advice. If i can't do it, what way should i use? Is there any good practices about it?
UPD:
I'm using global points to navigate between Fragments. It's how my navigation looks like:
The right|bottom chain is X, i can navigate from any of it to to not chanied fragments using bottom navigation. It's Single Activity app, to navigate i'm using just:
//Using global points
findNavController(R.id.host).navigate(R.id.toLibrary)
the following Solution employs Fragment Visibility Technique in order to Manage Fragments Backstack when onBackPressed().
at first step
We Assign a Tag to each Fragment when it's Called & Invoked to make it possible to Recognize which of Fragments have been Added to Backstack and are predecessors.
by below code, we assign a tag to a Fragment that is going to be Transacted.
fragmentManager.beginTransaction()
.replace(R.id.FragmentHolder, Fragment_A OR Fragment_B OR Fragment_ANY, "A or B or ANY")
.addToBackStack(tag).commit();
Remember you must assign a tag to any Fragment that you want to be Transacted.
In the second
We are going to Handling public void onBackPressed().
You MUST ignore super.onBackPressed() Because, we don't Want to defualt onBackPressed Method to Impact Backstack(as it is) while back button is pressed. Moreover, we Want to Handle Backstack ourselves.
Here you go
#Override
public void onBackPressed() {
if(isVisible("A")) { //Go some where else you wanna go }
else if(isVisible("B")){ //Go some where else you wanna go }
else if(isVisible("X or any"){ //Go some where else you wanna go }
else { //Go default page }
}
public boolean isVisible(String tag){
try {
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null && fragment.isVisible())
return true;
else
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
When back button is pressed, We check which Fragment is already Visible and do Redirect the user to the Corresponding Predecessor Fragment.
For Ex: A - > B OR A -> X OR X -> B
I am using this Technique for a Released Application and all good.
In fragmentTransaction instead of using addToBackStack() or add(...) use fragmentTransaction.replace(int containerViewId, Fragment fragment, String tag)
Related
This question already has answers here:
How to keep only one instance of a fragment, when switching with NavigationDrawer?
(4 answers)
Closed 2 years ago.
I have one Activity. In this activity, multiple fragments are there.
Fragments are in sequence to add,
A -> B -> C -> D -> B -> C -> D
Now, when i back action perform than sequence is,
D <- C <- B <- D <- C <- B <- A
But I have to perform back stack like this,
D <- C <- B <- A
what is the proper way to prevent the same Fragment in the backStack?
Here is my code for adding fragments,
if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
if (bundle != null)
fragment.arguments = bundle
transaction.add(R.id.container_body, fragment)
transaction.addToBackStack(fragTag)
// Commit the transaction
transaction.commit()
}
And also for backstack perform,
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate()
}
I don't know if this is perfect, but once I had done something like this. I had a stack of type integers (let's call it stackOfFragmentTracker). So suppose you have fragments f1, f2, f3, f4 and you did f1->f3->f4->f2. Then stackOfFragmentTracker would have content like this: 1->3->4->2.
Next, you create only one instance per fragment (so in my example, say f1, f2, f3, f4 each have only one instance) and you would keep them in backStack in INVISIBLE state:
final int ONE = 1; // a map to Fragment 1
final int TWO = 2; // a map to fragment 2
stackOfFragmentTracker.push(ONE);
ExampleFragment f1 = null;
if(fragmentManager.findFragmentByTag(ExampleFragment.TAG)==null){
f1 = new ExampleFragment();
fragmentManager.beginTransaction()
.add(R.id.flToMountOn, f1, ExampleFragment.TAG)
.addToBackStack(ExampleFragment.TAG)
.hide(f1) // <------- this makes your fragment invisible
.commit();
}
You would do that for all of your fragments.
Next, you need a Fragment variable (say, Fragment active), point it to your first fragment (i.e, where you want to go for the 1st time) and make it visible.
active = f1;
fragmentManager.beginTransaction().show(f1).commit();
To keep things clean, you can use these 2 methods:
private void hideActiveFragment(){
if(currentlyActiveFragment!=null){
fragmentManager.beginTransaction().hide(currentlyActiveFragment).commit();
}
}
private void showActiveFragment(Fragment nextFragment){
active = nextFragment;
if(active!=null){
fragmentManager.beginTransaction().show(active).commit();
}
}
Finally, whenever you go forward, you would push a number into stackOfFragmentTracker and simply make that fragment visible:
// say we are going to f2
void switchFragment(int n){
stackOfFragmentTracker.push(n); // you may keep a check if n is valid or not
if(n==1){
hideActiveFragment();
showActiveFragment(f1);
}else if(n==2){
hideActiveFragment();
showActiveFragment(f2);
} // ... ... ...
}
And onBackPress do this:
void insideBackPress(){
stackOfFragmentTracker.pop();
int top = stackOfFragmentTracker.top();
if(top==1){
hideActiveFragment();
showActiveFragment(f1);
}else if(top==2){
hideActiveFragment();
showActiveFragment(f2);
} // ... ... ...
}
I know the example is a bit messy, but I hope you get the point. Add necessary checks for various corner cases (if stackOfFragmentTracker isEmpty), or use a HashMap instead of the if-else block in my example. Good luck.
When creating the fragment set a tag for it, then later you can find it through the fragment manager and replace/create new one accordingly
FragmentManager fManager = getFragmentManager();
FragmentTransaction fTransaction = fManager.beginTransaction();
Fragment fragment = fManager.findFragmentByTag("uniqueTag");
// If fragment doesn't exist yet, create one
if (fragment == null) {
fTransaction.add(R.id.fragment_list, new ListFrag(), "uniqueTag");
}else { // re-use the old fragment
fTransaction.replace(R.id.fragment_list, fragment, "uniqueTag");
}
you can use dismiss() like if you go A to B, B To C and C to D when you press back and dismiss the fragment its removed from queue like its go like this D to C and C to B and B to A its make your stack clear and essay to handle
I have implemented a bottomNavigationView which each option has its entry fragment and some has more navigation under the same option.
Somehow like this:
A->A1
B->B1
C->C1->C2
D->D1
E->E1->E2
Where A,B,C,D & E are the options (MenuItem) for the bottom navigation view and A1,B1,C1,D1,E1 are the entry fragments for those options repectivly
The the desired navigation is that the entry AND exit point of the app will always be option A (entry fragment A1). So if the user navigates to another option as long as is in the entry fragment for that option, the behaivor for any back navigation should be to go to option A.
The problem I have is that the bottomNavigationView is always present as a requirement so the user could navigate to any option at any time.
For exemple if the user navigates to option E then in E1 takes an action that navigates to E2 an then navigates to option B if the user press the back button the app should go to option A because is in the entry fragment B1.
Also if the user navigates to option A using the bottom navigation view an then press the back button, as we are in the exit point we should be finishing the app.
In the OnNavigationItemSelectedListener I replace the current fragment for the entry fragment for the selected option using beginTransaction.replace for any options other than option A I add the addToBackStack(null) but this alone does not matches the desired navigation since if the user press the back button, insted of navigate to option A it navigates to the previous selected option. A have also tried to pop the back satck before replacing the fragment using popBackStack(BACK_STACK_HOME_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) and replacea adding addToBackStack(BACK_STACK_HOME_TAG) but somehow when selected a second option instead of showing the entry fragment for the selected option it shows the Fragment A1
navBar.setOnNavigationItemReselectedListener {
when(it.itemId) {
R.id.optionA -> {
// Removes all entries in the backstack if any
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack(
null,FragmentManager.POP_BACK_STACK_INCLUSIVE
)
return#setOnNavigationItemSelectedListener true
}
// Replaces/add the entry fragment
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentHost, FragmentA1())
.commit()
return#setOnNavigationItemSelectedListener true
}
R.id.optionB -> {
// Removes all entries in the backstack up to BACK_STACK_HOME_TAG
supportFragmentManager.popBackStack(
BACK_STACK_HOME_TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
// Replace the fragment with the entry FragmentB1
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentHost, FragmentB1())
.addToBackStack(BACK_STACK_HOME_TAG)
.commit()
return#setOnNavigationItemSelectedListener true
}
R.id.optionC -> {
// Removes all entries in the backstack up to BACK_STACK_HOME_TAG
supportFragmentManager.popBackStack(
BACK_STACK_HOME_TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
// Replace the fragment with the entry FragmentC1
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentHost, FragmentC1())
.addToBackStack(BACK_STACK_HOME_TAG)
.commit()
return#setOnNavigationItemSelectedListener true
}
...
return#setOnNavigationItemSelectedListener false
}
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
return
}
super.onBackPressed()
}
You should just override the Activity.OnBackPressed() method in a BaseActivity which every of your activities will inherit.
In that function, just check if the current Activity is of type A. If yes, exit the app, otherwise, launch Activity A.
Set BACK_STACK_HOME_TAG state only when adding fragment A1. No need to pop back state when adding fragment B1, C1, D1...
Override onKeyDown() in fragment A1, make it quit app:
System.exit(0);
Override onKeyDown() in fragment B1, C1, D1..., set it back to A1 like:
fragmentManager().popBackStack(BACK_STACK_HOME_TAG, 0);
For A2, B2, C2, D2..., just pop itself to go back to A1, B1, C1, D1...
fragmentManager().popBackStack();
I have here a sample GitHub project. It's just an activity inflating other fragments in it, by tapping bottom tabs:
Applaunch inflates Fragment 1
Pressing 1.Tab -> inflates Fragment 1
Pressing 2.Tab -> inflates Fragment 2
Pressing 3.Tab -> inflates Fragment 3
and all used by means of this.supportFragmentManager?.beginTransaction()?.replace(). For me it acts like a FragmentTransaction.add(), because:
When I start the app (Fragment 1 is loaded) and press
2nd Tab for Fragment 2 (Toast Frag 2)
and 3rd Tab for Fragment 3 (Toast Frag 3)
then backbutton (Toast Frag 2)
again backbutton (Toast Frag 1)
All the stack is working back. So nothing was "replaced", everything was added?
Whenever I press back, I would like to load "Fragment 1", which is the initial Fragment for the activity. How?
I guess it is happening because the fragments are being replaced but they are also being added to the BackStack as follows:
BaseActivity.kt
// Note the .addToBackStack(null)
this.supportFragmentManager?.beginTransaction()?.replace(resId, newFragment)?.addToBackStack(null)?.commit()
From Documentation for addToBackStack
Add this transaction to the back stack. This means that the transaction will be remembered after it is committed, and will reverse its operation when later popped off the stack.
You may want to check this question to check more info about the difference between add, replace and addToBackStack.
If you don't wanna to "remember and restore" the fragment transition, just remove the call to addToBackStack.
Edit
If you always want to return to first fragment, you can do:
Keep the addToBackStack(). So, the navigation history will be retained
this.supportFragmentManager?.beginTransaction()?.replace(resId,
newFragment)?.addToBackStack(null)?.commit()
Override onBackPressed on MainActivity or BaseActivity and request to always return to first transaction commit:
override fun onBackPressed() {
if(this.supportFragmentManager?.getBackStackEntryCount() > 0) {
this.supportFragmentManager?.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
} else {
super.onBackPressed()
}
}
Just note this is a test code and I could not verify. Also, I'm not familiar with Kotlin. So, this was the best code I could come with... Test and let me know the result... For any case, you can get the ideia.
Another option:
Remove the addToBackStack():
this.supportFragmentManager?.beginTransaction()?.replace(resId, newFragment)?.addToBackStack(null)?.commit()
Then, you must manually control which fragment should be displayed when user presses the back key... something like:
private var int : mCurrentFragment = 1
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
mCurrentFragment = 1
...
}
R.id.navigation_dashboard -> {
mCurrentFragment = 2
...
}
R.id.navigation_notifications -> {
mCurrentFragment = 3
...
}
}
false
}
override fun onBackPressed() {
if(mCurrentFragment != 1) {
replaceFragment(R.id.container_main_layout, Fragment1())
} else {
super.onBackPressed()
}
}
I currently have an android app that flows A -> B -> C -> D Launched in activity 1.
After D, I have activity 2 start and the following code runs to remove all fragments from the stack.
List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (fragments != null) {
for (Fragment fragment : fragments) {
getSupportFragmentManager().beginTransaction().remove(fragment).commit();
}
}
My problem is that when I return to activity 1 from activity 2, is that when I press the back key Fragment C animates back to screen. Fragment D does remove but A B C do not. I would like the app to close on backpress from activity 1.
I attached the code that removes my fragments here. Debugging shows that fragment does change with each loop.
Can anyone advise what is happening here?
You can overwrite backpressed method and close the app, like this:
#Override
public void onBackPressed() {
finish(); // finish activity
}
I'm using one Activity which contains one Fragment at a time. I use support.v4.
The "onBackPressed" of Activity and Fragment is override, so I can't use the original back button, I have to override it myself.
So I'm in fragment A, I go to fragment B. Fragment B displays a list of results, and when I pick a line, it goes to Fragment C.
When I'm on C and I press back button, I want to go back on B with the results displayed. And if I press back button again, I'll go to A.
It looks simple.
I manage the come back from C to B. The thing is when I hit the back button on B, it calls Fragment C backPressed.
A -> B -> C -> B (display B but BackButton don't work anymore).
Some code :
backPressed in Fragment C
public void backPressed() {
FragmentTransaction transaction = ((FragmentActivity)m_context).getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.mainactivity_fragmentcontainer,((FragmentActivity)m_context).getSupportFragmentManager().findFragmentByTag("customtag"), "customtag");
transaction.commitAllowingStateLoss();
}
Fragment B has the tag "customtag".
When I replace B by C:
FragmentC newFrag = new FragmentC(m_context);FragmentTransaction transac = ((FragmentActivity)m_context).getSupportFragmentManager().beginTransaction();
((FragmentContainerActivity)m_context).setCurrentFragment(newFrag);
transac.replace(R.id.mainactivity_fragmentcontainer, newFrag);
transac.addToBackStack(null);
transac.commitAllowingStateLoss();
Any idea why the fragmentC which is replaced (the display changes and I can see fragment B) still get the back button action?
Thanks for your help.
EDIT
Code of backPressed in Fragment B (however the application never goes there when it comes from Fragment C).
#Override
public void backPressed() {
Log.e("FragmentB", "backPressed");
//I have 2 views in FragB. If View2 is visible, I make it gone and display View1
if(mainView.findViewById(R.id.view2).getVisibility()==View.VISIBLE){
displayView1();
}else{
if(fromFragmentX){
FragmentX fragX = new FragmentX(ctx);
FragmentTransaction transac = ((FragmentActivity)ctx).getSupportFragmentManager().beginTransaction();
((FragmentContainerActivity)ctx).setCurrentFragment(fragX);
transac.replace(R.id.mainactivity_fragmentcontainer, fragX);
transac.commitAllowingStateLoss();
}else{
super.backPressed();
}
}
}
SECOND EDIT
I realize I forgot to precise something.
When I'm replacing fragment B by fragment C, I'm doing it in the ArrayAdapter of the ListView in Fragment B, but it shouldn't change anything.
Anyway, I tried something different somewhere else in the app.
In FragmentU, I replace FragmentU by Fragment C (always the same), and I'm setting an attribute in Fragment C which is :
private CustomFragment fragmentFrom;
So on backPressed in FragmentC, in case I'm from Fragment U, I call :
FragmentTransaction transaction = ((FragmentActivity)ctx).getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.mainactivity_fragmentcontainer, fragmentFrom);
transaction.commitAllowingStateLoss();
And I still got the same problem. When coming back from Fragment C, the Fragment U is displayed, and works well, but the back button call FragmentC.backPressed.