Handling back button press Inside Fragments

This is the fourth part of a 6 posts series on Fragment oriented application architecture. In the previous post I talked about Inter-Fragment Communication. In this part I am going discuss about elegantly handling back button press inside fragments in a fragment oriented application.

(Sample application’s source code and README)

Android devices have a hardware back button which normally serves the purpose of going back through the screens/actions stack. Callback to a back button press event is received in the foreground Activity (onBackPressed() event callback) which can be overridden and handled.

// GENERIC ACTIVITY LEVEL BACK PRESS HANDLING

@Override
public void onBackPressed() {
    if(some condition) {
        // do something
    } else {
        super.onBackPressed();
    }
}

But fragments do not get notified of a back press event. Suppose an Activity has a stack of 3 fragments. For a regular android user, normal expectation upon a back press would be to navigate back through the fragments. Of course, we would have added all the transactions to BackStack and now can enjoy the privilege of using popBackStack() method, but each fragment may have one or more special situations to consume back press. Handling back press for each of fragments inside the onBackPressed() call back of activity is the obvious choice but if the activity is supposed to host a good number of fragments, its onBackPressed() callback is likely to get cluttered and messed up.

For instance, in a ListFragment, a list row might expand upon clicking and a subsequent back press requires to collapse the expanded row.

// A SAMPLE CLUTTERED onBackPressed() CALLBACK IN AN ACTIVITY
// WHICH MAINTAINS A REFERENCE TO THE CURRENTLY ACTIVE FRAGMENT.

@Override
public void onBackPressed() {
    if(selectedFragment.equals(fragmentA) && fragmentA.hasExpandedRow()) {
        fragmentA.collapseRow();
    } else if(selectedFragment.equals(fragmentA) && fragmentA.isShowingLoginView()) {
        fragmentA.hideLoginView();
    } else if(selectedFragment.equals(fragmentA)) {
        popBackStack();
    } else if(selectedFragment.equals(fragmentB) && fragmentB.hasCondition1()) {
        fragmentB.reverseCondition1();
    } else if(selectedFragment.equals(fragmentB) && fragmentB.hasCondition2()) {
        fragmentB.reverseCondition2();
    } else if(selectedFragment.equals(fragmentB)) {
        popBackStack();
    } else {
        // handle by activity
        super.onBackPressed();
    }
}

This mess would grow exponentially with an increase in number of Fragments and complexity of each of them. Not clean!

After hours of hit and trials, I’ve finally come up with a strategy to cleanly propagate the back press event down to fragments, and let activity handle it only if the event doesn’t get consumed at fragment level. Enters the BackHandledFragment! An abstract base class that conveniently provisions to let a child active fragment utilize back press events. Host activity keeps track of its active fragment and in its onBackPressed() callback, it first checks if the active fragment requires to consume the back press, before handling it by itself.

// BASIC IMPLEMENTATION OF BACK HANDLED FRAGMENT

public abstract class BackHandledFragment extends Fragment {
    protected BackHandlerInterface backHandlerInterface;    
    public abstract String getTagText();
    public abstract boolean onBackPressed();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
	if(!(getActivity()  instanceof BackHandlerInterface)) {
	    throw new ClassCastException("Hosting activity must implement BackHandlerInterface");
	} else {
	    backHandlerInterface = (BackHandlerInterface) getActivity();
	}
    }
	
    @Override
    public void onStart() {
        super.onStart();
		
	// Mark this fragment as the selected Fragment.
	backHandlerInterface.setSelectedFragment(this);
    }
	
    public interface BackHandlerInterface {
	public void setSelectedFragment(BackHandledFragment backHandledFragment);
    }
}   

This class declares an abstract method onBackPressed(). Child fragments would override this method to consume the back press and return a boolean to tell if back-press event was consumed.

onCreate() callback is enforcing the implementation of BackHandlerInterface, which has a method to mark a Fragment as the selected (or active) fragment of the hosting Activity. onStart() calls this communicator method to update selected fragment to current fragment (using this keyword).

Following would be the resulting back handling related code in the activity.

// BASIC ACTIVITY CODE THAT LETS ITS FRAGMENT UTILIZE onBackPress EVENTS 
// IN AN ADAPTIVE AND ORGANIZED PATTERN USING BackHandledFragment

public class TheActivity extends FragmentActivity implements BackHandlerInterface {
    private BackHandledFragment selectedFragment;

    @Override
    public void onBackPressed() {
        if(selectedFragment == null || !selectedFragment.onBackPressed()) {
            // Selected fragment did not consume the back press event.
            super.onBackPressed();
        }
    }

    @Override
    public void setSelectedFragment(BackHandledFragment selectedFragment) {
        this.selectedFragment = selectedFragment;
    }
}

That’s it for back press handling!

Sample app is excessively using this method. BaseFragment in sample app is based on the above basic BackHandledFragment, thus, its inherited fragments are capable of handling back press events.

// GENERIC PATTERN OF OVERRIDING onBackPressed() METHOD
// INSIDE A FRAGMENT THAT EXTENDS BackHandledFragment.

@Override
public boolean onBackPressed() {
    if(condition 1) {
        // do something;
        return true; // event consumed
    } else if(condition 2) {
        // do something else;
        return true; // event consumed
    } else {
        // event not consumed, let Activity handle it.
        return false; 
    }
}

In the next post I’m going to talk about Centrally Managing Sessions in a Fragment oriented application, taking Facebook as an example.

12 thoughts on “Handling back button press Inside Fragments

  1. Could you not acquire the currently active fragment inside the activity by popping the backstack? It seems weird to me to have the fragment tell its activity what is currently supposed to be the active fragment.
    I haven’t tried this, but here’s how it could possibly work:

    public class MyActivity {
    public void newFragment(String fragmentTag) {
    MyFragment myFragment = MyFragment.newInstance(fragmentTag);
    FragmentManager.beginTransaction()
    // give the BackStackEntry the same name as the fragment
    .add(myFragment, fragmentTag)
    .addToBackStack(fragmentTag)
    .commit;
    }

    @Override
    public void onBackPressed() {
    //get the name from the topmost BackStackEntry which is also the fragment tag.
    String fragmentTag =
    FragmentManager.getBackStackEntryAt(FragmentManager.getBackStackCount()-1)
    .getName();

    MyFragment topmostFragment = FragmentManager.findFragmentByTag(fragmentTag);
    if (topmostFragment == null || !(topmostFragment instanceof BackHandledFragment) || !((BackHandledFragment) topmostFragment.onBackPressed())) {
    super.onBackPressed();
    }
    FragmentManager.popBackStack() //always pop?
    }

    }

    By having the same name for the BackStackEntry as for its associated Fragment you could avoid the BackHandlerInterface altogether which I think is preferable since you generally don’t want Fragments interfering with the Activity’s interchanging of Fragments.
    But like I said, it’s just a thought. I haven’t tried if this actually works.

  2. A clever idea. Thanks for posting.

    I think this may run into difficulties if you have an application built around multiple fragments where only one or two is using the BackHandledFragment mechanism. After the BackHandledFragment has come and gone and another (non-BackHandledFragment) fragment has become active, the activity is left with a reference to a stale fragment in “selectedFragment”. This object won’t be recycled (memory leak) because an active Activity holds a reference to it, and this fragment’s onBackPressed() method will still be called, even though it is not the active fragment with which the user was interacting when pressing Back.

    Chris’ proposal looks like a viable way to address those issues. No stale references to obsolete fragments, and only calling fragments if they are at the top of the stack.

  3. Has anyone actually made this work? From what I’ve learned in my efforts, Activity.onBackPressed() doesn’t actually get called unless the back-press would result in Activity.finish() being called. In other words, when a back-stacked fragment is on the top of the stack, Activity.onBackPressed() does _not_ get called as Android sends the event to the fragment manager instead, so this mechanism can’t forward that event to the fragment.

  4. I have two fragments and on each fragment their is one API call.But I want that when I press back button from second fragment then it’s simply removed and make first fragment visible .

    Basically I want to avoid API call on OnBackPress() method.

    How can I accomplish this?

  5. Thank you for the blog. By following your code I was implement the Back Fragment. And now my fragment can handle back presses!!! Thank you so much. Very simple and elegant solution.

  6. A very great idea and it works effectively. One thing I am stuck at is.
    Now when I press the back button in the fragment I come back to the activity(mission 1 complete)
    Now in the activity when I press back button, the same activity is called again and again. What i want to do is when the context is my current activity and when I press back button something else should happen. It would be really great if anybody could provide the solution to this. Thanks

  7. Wow!! Amazing.. worked perfectly for me! Thanks a lot for a simple and straight forward approach

Leave a Reply

Your email address will not be published. Required fields are marked *