Skip to content

Instantly share code, notes, and snippets.

@andreynovikov
Last active June 14, 2020 17:43
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save andreynovikov/4619215 to your computer and use it in GitHub Desktop.
Save andreynovikov/4619215 to your computer and use it in GitHub Desktop.
Complete working solution for Android action bar tabs with fragments having separate back stack for each tab.
// Custom interface that enables communication between Fragment and its Activity
public interface OnItemSelectedListener
{
public void onItemSelected(String item);
}
/*
* This code would not compile as-is. It will require custom Fragment classes, fragment interaction
* interfaces, animation resources. But it's fully functional in terms of correctly manipulating
* individual back stacks for each tab, preserving state not only on tab switch but also on device
* rotation and application switch. To work properly your Fragments should use setRetainInstance(true)
* or implement their individual state saving via onSaveInstanceState() and state restoring.
*
* This code is intentionally as simple as it can be, you can extend it to support custom animations,
* variable number of tabs, action bar 'up' navigation, etc.
*/
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.SherlockFragmentActivity;
// OnItemSelectedListener is a custom interface that enables communication between Fragment and
// its Activity (see bottom of the code for more information)
public class TabActivity extends SherlockFragmentActivity implements ActionBar.TabListener, OnItemSelectedListener
{
enum TabType
{
SEARCH, LIST, FAVORITES
}
// Tab back stacks
private HashMap<TabType, Stack<String>> backStacks;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Initialize ActionBar
ActionBar bar = getSupportActionBar();
bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
// Set back stacks
if (savedInstanceState != null)
{
// Read back stacks after orientation change
backStacks = (HashMap<TabType, Stack<String>>) savedInstanceState.getSerializable("stacks");
}
else
{
// Initialize back stacks on first run
backStacks = new HashMap<TabType, Stack<String>>();
backStacks.put(TabType.SEARCH, new Stack<String>());
backStacks.put(TabType.LIST, new Stack<String>());
backStacks.put(TabType.FAVORITES, new Stack<String>());
}
// Create tabs
bar.addTab(bar.newTab().setTag(TabType.SEARCH).setText("Search").setTabListener(this));
bar.addTab(bar.newTab().setTag(TabType.LIST).setText("List").setTabListener(this));
bar.addTab(bar.newTab().setTag(TabType.FAVORITES).setText("Favorites").setTabListener(this));
}
@Override
protected void onResume()
{
super.onResume();
// Select proper stack
Tab tab = getSupportActionBar().getSelectedTab();
Stack<String> backStack = backStacks.get(tab.getTag());
if (! backStack.isEmpty())
{
// Restore topmost fragment (e.g. after application switch)
String tag = backStack.peek();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment.isDetached())
{
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.attach(fragment);
ft.commit();
}
}
}
@Override
protected void onPause()
{
super.onPause();
// Select proper stack
Tab tab = getSupportActionBar().getSelectedTab();
Stack<String> backStack = backStacks.get(tab.getTag());
if (! backStack.isEmpty())
{
// Detach topmost fragment otherwise it will not be correctly displayed
// after orientation change
String tag = backStack.peek();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
ft.detach(fragment);
ft.commit();
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);
// Restore selected tab
int saved = savedInstanceState.getInt("tab", 0);
if (saved != getSupportActionBar().getSelectedNavigationIndex())
getSupportActionBar().setSelectedNavigationItem(saved);
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
// Save selected tab and all back stacks
outState.putInt("tab", getSupportActionBar().getSelectedNavigationIndex());
outState.putSerializable("stacks", backStacks);
}
@Override
public void onBackPressed()
{
// Select proper stack
Tab tab = getSupportActionBar().getSelectedTab();
Stack<String> backStack = backStacks.get(tab.getTag());
String tag = backStack.pop();
if (backStack.isEmpty())
{
// Let application finish
super.onBackPressed();
}
else
{
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
// Animate return to previous fragment
ft.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left);
// Remove topmost fragment from back stack and forget it
ft.remove(fragment);
showFragment(backStack, ft);
ft.commit();
}
}
@Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
{
// Select proper stack
Stack<String> backStack = backStacks.get(tab.getTag());
if (backStack.isEmpty())
{
// If it is empty instantiate and add initial tab fragment
Fragment fragment;
switch ((TabType) tab.getTag())
{
case SEARCH:
fragment = Fragment.instantiate(this, SearchFragment.class.getName());
break;
case LIST:
fragment = Fragment.instantiate(this, ListFragment.class.getName());
break;
case FAVORITES:
fragment = Fragment.instantiate(this, FavoritesFragment.class.getName());
break;
default:
throw new java.lang.IllegalArgumentException("Unknown tab");
}
addFragment(fragment, backStack, ft);
}
else
{
// Show topmost fragment
showFragment(backStack, ft);
}
}
@Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
// Select proper stack
Stack<String> backStack = backStacks.get(tab.getTag());
// Get topmost fragment
String tag = backStack.peek();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
// Detach it
ft.detach(fragment);
}
@Override
public void onTabReselected(Tab tab, FragmentTransaction ft)
{
// Select proper stack
Stack<String> backStack = backStacks.get(tab.getTag());
if (backStack.size() > 1)
ft.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left);
// Clean the stack leaving only initial fragment
while (backStack.size() > 1)
{
// Pop topmost fragment
String tag = backStack.pop();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
// Remove it
ft.remove(fragment);
}
showFragment(backStack, ft);
}
private void addFragment(Fragment fragment)
{
// Select proper stack
Tab tab = getSupportActionBar().getSelectedTab();
Stack<String> backStack = backStacks.get(tab.getTag());
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
// Animate transfer to new fragment
ft.setCustomAnimations(R.anim.slide_from_left, R.anim.slide_to_right);
// Get topmost fragment
String tag = backStack.peek();
Fragment top = getSupportFragmentManager().findFragmentByTag(tag);
ft.detach(top);
// Add new fragment
addFragment(fragment, backStack, ft);
ft.commit();
}
private void addFragment(Fragment fragment, Stack<String> backStack, FragmentTransaction ft)
{
// Add fragment to back stack with unique tag
String tag = UUID.randomUUID().toString();
ft.add(android.R.id.content, fragment, tag);
backStack.push(tag);
}
private void showFragment(Stack<String> backStack, FragmentTransaction ft)
{
// Peek topmost fragment from the stack
String tag = backStack.peek();
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
// and attach it
ft.attach(fragment);
}
// The following code shows how to properly open new fragment. It assumes
// that parent fragment calls its activity via interface. This approach
// is described in Android development guidelines.
@Override
public void onItemSelected(String item)
{
ItemFragment fragment = new ItemFragment();
Bundle args = new Bundle();
args.putString("item", item);
fragment.setArguments(args);
addFragment(fragment);
}
}
@steveoleary
Copy link

Excellent work, really helped me, thanks

@Simulant87
Copy link

WOW this really helps me a lot. Can I repost this on Stackoverflow to conserve this solution for my self (in public)? It took me more than a week to find this here and it is the best working snippet I have found.

@andreynovikov
Copy link
Author

Sure

@DanielNovak
Copy link

This works but it uses ft.add instead of the more common used ft.replace. The onStop/onStart/onPause/onResume lifecycle methods will not be called on fragment A if you add fragment B on top. I tried this with ft.replace but it's not that easy - because the previous fragments can't be found by tag (fragmentmanager does not keep a track of them). So I guess I would have to save the state of the fragments and then recreate them...:-(.

@ogero
Copy link

ogero commented Oct 24, 2014

Tip to make this also change the actionbar title and show the back arrow on child stack fragments:

//StackedFragment.java (implemented by all or some root or child fragments)
public interface StackedFragment {
    //Return true if fragment needs to show back arrow on ActionBar
    public boolean getDisplayHomeAsUpEnabled();
    //Return title to be shown on ActionBar when fragment added
    public String getTitle(Context context);
}
//TabActivity.java
public class TabActivity extends ActionBarActivity implements ActionBar.TabListener, OnItemSelectedListener  {

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                onBackPressed();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void addFragment(Fragment fragment, Stack<String> backStack, FragmentTransaction ft) {
        // . . .
        updateActionBar(fragment);
    }

    private void showFragment(Stack<String> backStack, FragmentTransaction ft) {
        // . . .
        updateActionBar(fragment);
    }

    private void updateActionBar(Fragment fragment) {
        ActionBar actionBar = getSupportActionBar();
        if (fragment instanceof StackedFragment) {
            StackedFragment tF = (StackedFragment) fragment;
            actionBar.setDisplayHomeAsUpEnabled(tF.getDisplayHomeAsUpEnabled());
            actionBar.setTitle(tF.getTitle(this));
        } else {
            actionBar.setDisplayHomeAsUpEnabled(false);
            actionBar.setTitle(getString(R.string.app_name));
        }
    }

}

@pumaloso
Copy link

pumaloso commented Nov 6, 2014

There's an issue here:

Stack<String> backStack = backStacks.get(tab.getTag());

When the android OS sends the application to the background, so that when it comes from background, throws the error:

java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.util.Stack

@gohilronak
Copy link

We can use this method: http://developer.android.com/training/basics/fragments/fragment-ui.html#Replace

FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
transaction.commit();

FragmentTransaction.addToBackStage() method saves the transaction, and reverses the transaction when back button is pressed.

Using this method, when user presses back button, it shows previous fragment of current tab, & when fragment stack gets empty of current tab, it exits the app.

@samtipton
Copy link

As for the comment above, this defeats the entire purpose of the gist. OP is trying to manage separate 'backstacks' for each tab in his activity.

As for this gist in general, while OP is detaching (deflating UI components) past fragments he is not removing these fragment states from memory or reversing the general case of the transaction that added it. So it fails to fully stand in for the function of the backstack which is to parcel the fragment transaction and relevant fragment state to disk and reverse the transaction on reattach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment