Synthesize Android fragment backstack - android

I have an activity that uses fragments to change views instead of launching new activities. Lets say I have 3 fragments A, B and C. When the application launches the default fragment is set to A. The user can click a button on A to transition to B -- same with B to C.
Therefore the backstack looks like:
[A] -> [B] -> [C]
What I need to do is deep link directly to fragment C from a notification while still building out the backstack so that when the activity is launched. Fragment C should be displayed while allowing the user to click the back button to get back to views B and A respectively.

You can make 3 separate transactions. This is a lot more natural than manually checking the state of the backstack.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showFragmentA();
if (getIntent().hasExtra("some_deep_link_flag")) {
showFragmentB();
showFragmentC();
}
}
private void showFragmentA() {
Fragment a = new Fragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, a)
.addToBackStack(null)
.commit();
}
private void showFragmentB() {
Fragment b = new Fragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, b)
.addToBackStack(null)
.commit();
}
private void showFragmentC() {
Fragment c = new Fragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, c)
.addToBackStack(null)
.commit();
}

If the stack is always going to A -> B -> C then you could override onBackPressed() in the activity and pop the back stack and check to see what the situation in the fragment stack is.
#Override
public void onBackPressed() {
if (getSupportFragmentManager().getFragmentByTag("C") != null) {
if (getSupportFragmentManager().getBackStackEntryCount() == 2) {
getSupportFragmentManager().beginTransaction.replace(...).commit();
} else {
getSupportFragmentManager().popBackStack();
}
} else if (getSupportFragmentManager().getFragmentByTag("B") != null) {
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
getSupportFragmentManager().beginTransaction.replace(...).commit();
} else {
getSupportFragmentManager().popBackStack();
}
} else {
//You are already on Fragment A
super.onBackPressed();
}
}

Like jmcdonnell40 suggests:
My guess is you should put an extra on the notification intent which makes clear that you are entering your Activity from the notification.
Then, in the activity's oncreate, check for that extra, and if it is present, just do the 2 necessary fragment transactions (fragment A -> B -> C) and add them to the backstack.
you're going to have to open your last fragment manually anyway.

Related

Fragment does not pop out from backstack?

I have three fragment A, B and C
This is the path : A -> B -> C : A go to B and B go to C
When I go back from the fragment C , I want to go to the fragment A.
A <- C : C go back to the root fragment A
A <- B : B go back to the root fragment A
But My problem is when I pressed back in the fragment C , I get this behviour : It seems that the fragment C is not cleared from the backstack :
As for my code , this is the method of the replaceFragment :
public void replaceFragment(Fragment fragment, boolean withBackStack) {
android.support.v4.app.FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
String fragmentTag = fragment.getClass().getName();
fragmentTransaction.replace(R.id.frame, fragment, fragmentTag);
if (withBackStack)
fragmentTransaction.addToBackStack(fragmentTag);
try {
fragmentTransaction.commitAllowingStateLoss();
} catch (IllegalStateException ex) {
ex.printStackTrace();
}
}
First I called replaceFragment(new AFragment(), false); in the MainActivity , Then in the Fragment A when I clicked in the button, I called mListener.replaceFragment(new BFragment(), true);
Finally , in the Fragment B when I clicked the button , I called mListener.replaceFragment(new CFragment(), false);
Does anyone have an explanation for this behaviour ? The last fragment C shouldn't be cleared when I click backpressed ?
Here , you will find the whole example.
Thanks in advance!
Since you are not adding fragC to the backstack when backpressed wont pop the stored transaction it is not removed.instead here you have to remove the fragment by overriding backpress.Overiride backpress and check for the fragC and remove it and then call the pospstack to pop back the stored transaction.
Also store the instance of fragment as global to check if the fragment if fragC.
private Fragment mFragment;
Inside you method store the instance
#Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(mFragment.getClass().getName());
if (fragment != null && fragment.getClass().getName().equalsIgnoreCase(CFragment.class.getName())) {
getSupportFragmentManager().beginTransaction().remove(mFragment).commit();
}
getSupportFragmentManager().popBackStackImmediate();
} else {
super.onBackPressed();
}
}
Based on the answer of #Anonymous, I found a general solution for this problem :
#Override
public void onBackPressed() {
Fragment currentFragment= getSupportFragmentManager().findFragmentById(R.id.frame);
super.onBackPressed();
if(currentFragment!=null && currentFragment.isVisible()) {
getSupportFragmentManager().beginTransaction().remove(currentFragment).commit();
getSupportFragmentManager().popBackStack(currentFragment.getTag(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}

Exiting from Activity with only fragments

My app contains one empty activity and a couple of fragments. The onCreate of the activity replaces the empty view in activity_main.xml with a MainFragment that contains some buttons. Each button launches a separate fragment, and user can navigate from one fragment to another, etc.
On the press of back key, the current fragment correctly gets replaced with the previous fragment, until you get to the MainFragment. When user presses back from MainFragment, it hides the main fragment and you see the white empty background of the main activity. But I want to exit from the activity at this point, as that would be the sensible behaviour.
I am able to achieve this by calling super.onBackPressed() for a second time from onBackPressed if there are no fragments left in the fragment manager.
#Override
public void onBackPressed() {
super.onBackPressed();
FragmentManager manager = getSupportFragmentManager();
List<Fragment> fragments = manager.getFragments();
if (fragments == null || fragments.size() == 0) {
Log.d(TAG, "No more fragments: exit");
super.onBackPressed();
}
}
Is this acceptable thing to do - would it create any issues in the activity workflow? Is there a better/standard way to handle this scenario?
There is no problem to do that, but probably it would be easier if when you add the main fragment to the activity you do NOT call .addToBackStack()
You don't really need to override onBackPressed in your Activity. I would suggest implementing a method for adding fragments in your Activity:
protected void addFragment(Fragment fragment, boolean addToBackStack) {
String tag = fragment.getClass().getName(); //It's optional, may be null
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction()
.add(R.id.your_container_id, fragment, tag);
if (addToBackStack) {
transaction.addToBackStack(tag);
}
transaction.commit();
}
And modify your onCreate method of activity like in the following snippet:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
// Add your fragment only if it is a first launch,
// otherwise it will be restored by system
addFragment(new YourFirstFragment(), false);
}
}
For all other fragments use:
addFragment(new OtherFragment(), true);

OnBackPress the fragment to be shown fails to update

Suppose I'm in fragment A, then moving to B, then using Back button returns to A.
In the activity I'm performing the following override:
#Override
public void onBackPressed(){
FragmentManager fm = getSupportFragmentManager();
Fragment frag = fm.findFragmentByTag(Consts.A);
if (frag != null){
Log.d(Consts.TAGS.ACTIVITY_ORDER,"");
fm.beginTransaction().remove(frag).commit();
fm.popBackStack();
}
}
and while showing B goes like this:
FragmentManager fm = getActivity().getSupportFragmentManager();
Fragment f = BFragment.newInstance(Consts.B);
fm.beginTransaction()
.replace(R.id.rl_content,
f,
Consts.B)
.addToBackStack(null)
.commit();
Now, which method (if any) will be executed in A, once we execute popBackStack()?
If none, how can we change A's data models or UI components (such as keyboard or a TextView) right after back press? is it component-dependent?
R.id.rl_content is the container.
Please consider 2 cases:
1. A is in R.id.rl and being replaced
2. A is not in R.id.rl and is not being replaced
If you're always going back from Fragment B to Fragment A or vice versa, i would recommend this solution inside the fragments themselves.
#Override
public void onResume() {
super.onResume();
Fragment f = AFragment.newInstance(Consts.A);
if(getView() == null){
return;
}
getView().setFocusableInTouchMode(true);
getView().requestFocus();
getView().setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
FragmentTransaction trans = getFragmentManager().beginTransaction();
trans.replace(R.id.rl_content, f);
trans.addToBackStack(null);
trans.commit();
return true;
}
return false;
}
});
}
You can freely move from B to A and A to B using the same code. if you would like a more dynamic approach e.g. you would like to go from Fragment A to Fragment C, or Fragment B to Fragment C and then when you press back go back to the previous fragment on stack. I would aim to use Kyle Falconer's Solution here
Incase the link dies, I'll post the code here:
#Override
public void onBackPressed(){
FragmentManager fm = getFragmentManager();
if (fm.getBackStackEntryCount() > 0) {
Log.i("MainActivity", "popping backstack");
fm.popBackStack();
} else {
Log.i("MainActivity", "nothing on backstack, calling super");
super.onBackPressed();
}
}
I haven't tested the second solution, but use the first.
There are quiet a few ways by which you can change A's data models or UI components.
Case 1: when A is in R.id.rl_content and is being replaced by B. In this case you can simply update required models or UI in onCreateView of Fragment A.
Case 2: When A is not being replaced. In this case fragment A doesn't know when to update its view. In the onBackpressed() of your activity you can call Fragment A's updateView() method if Fragment B is being popped.
#Override
public void onBackPressed(){
FragmentManager fm = getSupportFragmentManager();
FragmentB fragmentB = (FragmentB)fm.findFragmentByTag(Consts.B);
if (fragmentB != null){
Log.d(Consts.TAGS.ACTIVITY_ORDER,"");
fm.beginTransaction().remove(fragmentB).commit();
fm.popBackStack();
FragmentA fragmentA = (FragmentA)fm.findFragmentByTag(Consts.A);
if (fragmentA != null) {
fragmentA.updateView();
}
}
}
EDIT
I understand that you also want to handle scenarios like hiding keyboard etc.
For this you might want to pass backpress event to the individual fragments. Somewhat like this:
#Override
public void onBackPressed(){
FragmentManager fm = getSupportFragmentManager();
FragmentB fragmentB = (FragmentB)fm.findFragmentByTag(Consts.B);
if (fragmentB != null){
if (!fragmentB.onBackPress()) {
// This means fragment B doesn't want to consume backpress therefore remove it.
Log.d(Consts.TAGS.ACTIVITY_ORDER,"");
fm.beginTransaction().remove(fragmentB).commit();
fm.popBackStack();
FragmentA fragmentA = (FragmentA)fm.findFragmentByTag(Consts.A);
if (fragmentA != null) {
fragmentA.updateView();
}
}
}
}
And in your Fragment B create a function onBackPress like this:
public boolean onBackPressed() {
// if keyboard is showing then hide it here and return true to consume the back press event or else return false to dismiss this fragment.
}

Is there anyway to swap or reorder fragment BackStack entry?

am creating multiple fragment app, which require easy changing of fragment tabs, whose BackStack entry must be kept. On user click fragment must be pop up immediate. I think reordering BackStack is the best option. Do anyone know how to reorder BackStack ?
I have 5 fragments A,B,C,D,E. if user open fragment like A ---> B ---> C ---> D ---> E . using default back stack,back key it works fine. But when user opens A ---> B ---> C ---> D ---> E ---> B. After this if user click back, default back stack will goes to A.
There is no api for this usecase, but there are other means to do so:
Example
FragmentBackStackModifyActivity
use non-swipable viewpager and on addOnPageChangeListener create arraylist and save all pages that opened. onBackPressed will compare arraylist and show pages.
The user follows the path A ---> B ---> C ---> D ---> E then he wants to come to B. So what you should do is:
check if B is already in the back stack or not.
If it is there, then popBackStackImmediate to return to B
If it is not there, then add the fragment B normally.
So replace fragment should be something like this:
public void replaceFragment(Fragment fragment) {
String fragmentTag = fragment.getClass().getCanonicalName(); //unique name for the fragment, which will be used as tag.
FragmentManager fragmentManager = getSupportFragmentManager();
boolean isPopped = fragmentManager.popBackStackImmediate(fragmentTag, 0); // trying to return to the previous state of B if it exists.
if (!isPopped && fragmentManager.findFragmentByTag(fragmentTag) == null){ // fragment is not in the backstack, so create it.
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.frame_layout, fragment, fragmentTag);
ft.addToBackStack(fragmentTag);
ft.commit();
}
}
Remember, this will return to the previous state of B.
TIP: Since you don't want two instances of any fragment, So try to create a singleton instance of them. Because, if you are creating a new instance of the fragment and that fragment already exists in the back stack then the new instance will be ignored (because you are returning to the previous state).
You Can Try this...
onBackpressed
#Override
public void onBackPressed() {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.frame_container);
if (f instanceof FragmentE)
{
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.popBackStack(fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount()-1).getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
} else if (f instanceof FragmentD) {
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.popBackStack(fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount()-2).getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
else if (f instanceof FragmentC) {
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.popBackStack(fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount()-3).getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
else if (f instanceof FragmentB) {
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.popBackStack(fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount()-4).getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
else
{
super.onBackPressed();
}
}
for back remove stack
final android.app.FragmentManager fm = getFragmentManager();
fm.addOnBackStackChangedListener(new android.app.FragmentManager.OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() ==0) {
// dLayout.closeDrawers();
finish();
}
else
{
// dLayout.closeDrawers();
}
}
});
May be call addToBackstack() only when the fragment is not added before
Suppose as in your case when user is in Fragment E and when user calls Fragment B check if the Fragment B is already added in back stack
if yes, Then dont call addToBackstack for that FragmentTransaction.
else ,call addToBackstack()
check my sample code:
private void addFragment (Fragment fragment){
boolean isFragment=false;
String backStateName = fragment.getClass().getName();
FragmentManager fm = getFragmentManager();
for(int entry = 0; entry < fm.getBackStackEntryCount(); entry++){
Log.i(TAG, "Found fragment: " + fm.getBackStackEntryAt(entry).getId());
if(fm.getBackStackEntryAt(entry).getId().equalsIgnoreCase(FragmentB))// check if fragment is in back stack
{
isFragment=true;
}
}
if (!isFragment){ //fragment added to back stack, create it.
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, FragmentB).addToBackStack(FragmentB).commit();
}else {//else dont call addToBackStack()
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, FragmentB).commit();
}
}
Just place this code in your MainActivity
#Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
//super.onBackPressed();
if (getFragmentManager().getBackStackEntryCount() == 1) {
finish();
}
getFragmentManager().popBackStack();
}
}
Here getFragmentManager().getBackStackEntryCount() == 0 will checks that whether the stack entry is at 0 or not, if it's on 0 then the Activity will be finished.
In your case A --> B --> C --> D --> E --> B
A will be on 0,
B will be on 1,
C on 2,
D on 3,
E on 4,
B on 5
So when you click on back, the if condition will checks that the backstack count is on 0.

Fragment backstack and toggle

I'm having an special use case where I need to switch between two fragments. The issue I'm having is that for the second fragment I need to persist it's state, and the only thing that seems to be working for that is to add it to the BackStack.
I rely on the support fragment manager to replace the fragments:
public void toggle() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment instanceof FragmentB && null != fragmentA) {
// fragment B is visible - we should show fragment A
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.frag_fade_in, R.anim.frag_fade_out,
R.anim.frag_fade_in, R.anim.frag_fade_out)
.replace(R.id.fragment_container, fragmentA)
.commit();
} else if (fragment instanceof FragmentA && null != fragmentB) {
// fragment A is visible - we should show fragment B
boolean isRestored = false;
fragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_B);
if (null != fragment) {
// Restore fragment state from the BackStack
fragmentB = (FragmentB) fragment;
isRestored = true;
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.frag_fade_in,
R.anim.frag_fade_out,
R.anim.frag_fade_in,
R.anim.frag_fade_out);
transaction.replace(R.id.fragment_container, fragmentB, TAG_FRAG_B);
if(!isRestored){
transaction.addToBackStack(TAG_FRAG_B)
}
transaction.commit();
} else {
// Just pop any fragments that were added - usually we won't get in here
getSupportFragmentManager().popBackStack();
}
}
This in combination with the onBackPressed() override:
#Override
public void onBackPressed() {
if (isCurrentFragmentB()) {
toggle();
} else {
// Back key was pressed and we are on fragment A - at this state we simply want to go back to the
// previous section
super.onBackPressed();
}
}
Using this implementation I make sure I reuse fragment B and keep it's state so that it doesn't look like it is created from scratch each time. I also make sure that when I go back, I can go only from fragment B to A and not from fragment A to B.
The issue I encountered is that when super.onBackPressed(); is called and more than one fragment was added(replaced actually, as I want only one active fragment at a time) through the fragment manager, it will throw an exception:
java.lang.IllegalStateException: Fragment already added: FragmentA{af9c26b #0 id=0x7f0e00d3}
This is happening only when the active fragment is FragmentA. I have a suspicion that this is because of the BackStack implementation, but as I've said, I only want the second one to be persisted.
How can I fix this? I am missing something?
I have managed to implement an work-around for this, although it is a little hacky.
Because I need to keep the state of FragmentB, I am forced to add it to the BackStack, but this will actually affect what transition is reversed when onBackPressed() is called.
To avoid this, I had to update the logic for the back press and manually handle that case
#Override
public void onBackPressed() {
if (isCurrentFragmentB()) {
toggle();
} else if (isCurrentFragmentA()) {
getSupportFragmentManager().popBackStackImmediate(TAG_FRAG_A, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// Special case - because we added the fragment B to the BackStack in order to easily resume it's state,
// this will fail as it will actually try to add fragment A again to the fragment manager (it
// will try to reverse the last transaction)
super.finish();
} else {
// Usual flow - let the OS decide what to do
super.onBackPressed();
}
}
Also, I've optimized the toggle method a little bit:
public void toggle() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
#SuppressLint("CommitTransaction") FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.frag_fade_in, R.anim.frag_fade_out,
R.anim.frag_fade_in, R.anim.frag_fade_out);
if (fragment instanceof FragmentB && null != fragmentA) {
// fragment B is visible - we should show fragment A
fragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_A);
if (null != fragment) {
// Restore fragment state from the BackStack
fragmentA = (FragmentA) fragment;
}
// Replace current fragment with fragment A and commit the transaction
transaction.replace(R.id.fragment_container, fragmentA, TAG_FRAG_A).commit();
} else if (fragment instanceof FragmentA && null != fragmentB) {
// fragment A is visible - we should show fragment B
fragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_B);
if (null != fragment) {
// Restore fragment state from the BackStack
fragmentB = (FragmentB) fragment;
}
// Replace current fragment with fragment B
transaction.replace(R.id.fragment_container, fragmentB, TAG_FRAG_B);
if (null == fragment) {
// No entry of the fragment B in the BackStack, we want to add it for future uses
transaction.addToBackStack(TAG_FRAG_B);
}
// Commit the transaction
transaction.commit();
} else {
// Just pop any fragments that were added - usually we won't get in here
getSupportFragmentManager().popBackStack();
}
}
I hope this can help others which need an similar flow.
PS: The fragment I want to persist is SupportMapFragment, so that my map isn't always redrawn, re-centered and populated with data every time I want to show it.

Categories

Resources