Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Separate Back Stack for each tab in Android using Fragments

I'm trying to implement tabs for navigation in an Android app. Since TabActivity and ActivityGroup are deprecated I would like to implement it using Fragments instead.

I know how to set up one fragment for each tab and then switch fragments when a tab is clicked. But how can I have a separate back stack for each tab?

For an example Fragment A and B would be under Tab 1 and Fragment C and D under Tab 2. When the app is started Fragment A is shown and Tab 1 is selected. Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed. If Tab 1 is then selected Fragment B should once again be displayed. At this point it should be possible to use the back button to show Fragment A.

Also, it is important that the state for each tab is maintained when the device is rotated.

BR Martin

like image 590
mardah Avatar asked Aug 08 '11 19:08

mardah


People also ask

What is the use of back stack in Android fragments?

The user can later reverse the transaction and bring back the previous fragment by pressing the Back button. If you added or removed multiple fragments within a single transaction, all of those operations are undone when the back stack is popped.

How can I maintain fragment state when added to the back stack?

Solution: Save required information as an instance variable in calling activity. Then pass that instance variable into your fragment.

Can a fragment without a layout can be attached to an activity?

A fragment is not required to be a part of the Activity layout ; you may also use a fragment without its own UI as an invisible worker for the Activity but it needs to be attached to an Activity in order to appear on the screen. Save this answer.

What is advantage of fragments in Android?

Fragments always exist within the context of a given Activity and can always access that Activity. By storing the information of interest within the Activity, the Fragment for each screen can simply access the object reference through the Activity.


2 Answers

I am terribly late to this question . But since this thread has been very informative and helpful to me I thought I better post my two pence here.

I needed a screen flow like this (A minimalistic design with 2 tabs and 2 views in each tab),

tabA     ->  ScreenA1, ScreenA2 tabB     ->  ScreenB1, ScreenB2 

I had the same requirements in the past, and I did it using TabActivityGroup (which was deprecated at that time too) and Activities. This time I wanted to use Fragments.

So this is how I done it.

1. Create a base Fragment Class

public class BaseFragment extends Fragment {     AppMainTabActivity mActivity;      @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         mActivity = (AppMainTabActivity) this.getActivity();     }      public void onBackPressed(){     }      public void onActivityResult(int requestCode, int resultCode, Intent data){     } } 

All fragments in your app can extend this Base class. If you want to use special fragments like ListFragment you should create a base class for that too. You will be clear about the usage of onBackPressed() and onActivityResult() if you read the post in full..

2. Create some Tab identifiers, accessible everywhere in project

public class AppConstants{     public static final String TAB_A  = "tab_a_identifier";     public static final String TAB_B  = "tab_b_identifier";      //Your other constants, if you have them.. } 

nothing to explain here..

3. Ok, Main Tab Activity- Please go through comments in code..

public class AppMainFragmentActivity extends FragmentActivity{     /* Your Tab host */     private TabHost mTabHost;      /* A HashMap of stacks, where we use tab identifier as keys..*/     private HashMap<String, Stack<Fragment>> mStacks;      /*Save current tabs identifier in this..*/     private String mCurrentTab;      protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.app_main_tab_fragment_layout);          /*            *  Navigation stacks for each tab gets created..           *  tab identifier is used as key to get respective stack for each tab          */         mStacks             =   new HashMap<String, Stack<Fragment>>();         mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());         mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());          mTabHost                =   (TabHost)findViewById(android.R.id.tabhost);         mTabHost.setOnTabChangedListener(listener);         mTabHost.setup();          initializeTabs();     }       private View createTabView(final int id) {         View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);         ImageView imageView =   (ImageView) view.findViewById(R.id.tab_icon);         imageView.setImageDrawable(getResources().getDrawable(id));         return view;     }      public void initializeTabs(){         /* Setup your tab icons and content views.. Nothing special in this..*/         TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);         mTabHost.setCurrentTab(-3);         spec.setContent(new TabHost.TabContentFactory() {             public View createTabContent(String tag) {                 return findViewById(R.id.realtabcontent);             }         });         spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));         mTabHost.addTab(spec);           spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);         spec.setContent(new TabHost.TabContentFactory() {             public View createTabContent(String tag) {                 return findViewById(R.id.realtabcontent);             }         });         spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));         mTabHost.addTab(spec);     }       /*Comes here when user switch tab, or we do programmatically*/     TabHost.OnTabChangeListener listener    =   new TabHost.OnTabChangeListener() {       public void onTabChanged(String tabId) {         /*Set current tab..*/         mCurrentTab                     =   tabId;          if(mStacks.get(tabId).size() == 0){           /*            *    First time this tab is selected. So add first fragment of that tab.            *    Dont need animation, so that argument is false.            *    We are adding a new fragment which is not present in stack. So add to stack is true.            */           if(tabId.equals(AppConstants.TAB_A)){             pushFragments(tabId, new AppTabAFirstFragment(), false,true);           }else if(tabId.equals(AppConstants.TAB_B)){             pushFragments(tabId, new AppTabBFirstFragment(), false,true);           }         }else {           /*            *    We are switching tabs, and target tab is already has atleast one fragment.             *    No need of animation, no need of stack pushing. Just show the target fragment            */           pushFragments(tabId, mStacks.get(tabId).lastElement(), false,false);         }       }     };       /* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/     public void setCurrentTab(int val){           mTabHost.setCurrentTab(val);     }       /*       *      To add fragment to a tab.       *  tag             ->  Tab identifier      *  fragment        ->  Fragment to show, in tab identified by tag      *  shouldAnimate   ->  should animate transaction. false when we switch tabs, or adding first fragment to a tab      *                      true when when we are pushing more fragment into navigation stack.       *  shouldAdd       ->  Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)      *                      true in all other cases.      */     public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){       if(shouldAdd)           mStacks.get(tag).push(fragment);       FragmentManager   manager         =   getSupportFragmentManager();       FragmentTransaction ft            =   manager.beginTransaction();       if(shouldAnimate)           ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);       ft.replace(R.id.realtabcontent, fragment);       ft.commit();     }       public void popFragments(){       /*            *    Select the second last fragment in current tab's stack..         *    which will be shown after the fragment transaction given below         */       Fragment fragment             =   mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);        /*pop current fragment from stack.. */       mStacks.get(mCurrentTab).pop();        /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/       FragmentManager   manager         =   getSupportFragmentManager();       FragmentTransaction ft            =   manager.beginTransaction();       ft.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);       ft.replace(R.id.realtabcontent, fragment);       ft.commit();     }          @Override     public void onBackPressed() {         if(mStacks.get(mCurrentTab).size() == 1){           // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..           finish();           return;         }          /*  Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action          *  when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()          *  kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.          */         ((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();          /* Goto previous fragment in navigation stack of this tab */             popFragments();     }       /*      *   Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function      *  in that fragment, and called it from the activity. But couldn't resist myself.      */     @Override     protected void onActivityResult(int requestCode, int resultCode, Intent data) {         if(mStacks.get(mCurrentTab).size() == 0){             return;         }          /*Now current fragment on screen gets onActivityResult callback..*/         mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);     } } 

4. app_main_tab_fragment_layout.xml (In case anyone interested.)

<?xml version="1.0" encoding="utf-8"?> <TabHost     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@android:id/tabhost"     android:layout_width="fill_parent"     android:layout_height="fill_parent">      <LinearLayout         android:orientation="vertical"         android:layout_width="fill_parent"         android:layout_height="fill_parent">          <FrameLayout             android:id="@android:id/tabcontent"             android:layout_width="0dp"             android:layout_height="0dp"             android:layout_weight="0"/>          <FrameLayout             android:id="@+android:id/realtabcontent"             android:layout_width="fill_parent"             android:layout_height="0dp"             android:layout_weight="1"/>          <TabWidget             android:id="@android:id/tabs"             android:orientation="horizontal"             android:layout_width="fill_parent"             android:layout_height="wrap_content"             android:layout_weight="0"/>      </LinearLayout> </TabHost> 

5. AppTabAFirstFragment.java (First fragment in Tab A, simliar for all Tabs)

public class AppTabAFragment extends BaseFragment {     private Button mGotoButton;      @Override     public View onCreateView(LayoutInflater inflater, ViewGroup container,             Bundle savedInstanceState) {         View view       =   inflater.inflate(R.layout.fragment_one_layout, container, false);          mGoToButton =   (Button) view.findViewById(R.id.goto_button);         mGoToButton.setOnClickListener(listener);          return view;     }      private OnClickListener listener        =   new View.OnClickListener(){         @Override         public void onClick(View v){             /* Go to next fragment in navigation stack*/             mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);         }     } } 

This might not be the most polished and correct way. But it worked beautifully in my case. Also I only had this requirement in portrait mode. I never had to use this code in a project supporting both orientation. So can't say what kind of challenges I face there..

EDIT :

If anyone want a full project, I have pushed a sample project to github.

like image 115
Krishnabhadra Avatar answered Sep 22 '22 00:09

Krishnabhadra


We had to implement exactly that same behaviour that you describe for an app recently. The screens and overall flow of the application were already defined so we had to stick with it (it's an iOS app clone...). Luckily, we managed to get rid of the on-screen back buttons :)

We hacked the solution using a mixture of TabActivity, FragmentActivities (we were using the support library for fragments) and Fragments. In retrospective, I'm pretty sure it wasn't the best architecture decision, but we managed to get the thing working. If I had to do it again, I'd probably try to do a more activity-based solution (no fragments), or try and have only one Activity for the tabs and let all the rest be views (which I find are much more reusable than activities overall).

So the requirements were to have some tabs and nestable screens in each tab:

tab 1   screen 1 -> screen 2 -> screen 3 tab 2   screen 4 tab 3   screen 5 -> 6 

etc...

So say: user starts in tab 1, navigates from screen 1 to screen 2 then to screen 3, he then switches to tab 3 and navigates from screen 4 to 6; if the switched back to tab 1, he should see screen 3 again and if he pressed Back he should return to screen 2; Back again and he is in screen 1; switch to tab 3 and he's in screen 6 again.

The main Activity in the application is MainTabActivity, which extends TabActivity. Each tab is associated with an activity, lets say ActivityInTab1, 2 and 3. And then each screen will be a fragment:

MainTabActivity   ActivityInTab1     Fragment1 -> Fragment2 -> Fragment3   ActivityInTab2     Fragment4   ActivityInTab3     Fragment5 -> Fragment6 

Each ActivityInTab holds only one fragment at a time, and knows how to replace one fragment for another one (pretty much the same as an ActvityGroup). The cool thing is that it's quite easy to mantain separate back stacks for each tab this way.

The functionality for each ActivityInTab was quite the same: know how to navigate from one fragment to another and maintain a back stack, so we put that in a base class. Let's call it simply ActivityInTab:

abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_in_tab);     }      /**      * Navigates to a new fragment, which is added in the fragment container      * view.      *       * @param newFragment      */     protected void navigateTo(Fragment newFragment) {         FragmentManager manager = getSupportFragmentManager();         FragmentTransaction ft = manager.beginTransaction();          ft.replace(R.id.content, newFragment);          // Add this transaction to the back stack, so when the user presses back,         // it rollbacks.         ft.addToBackStack(null);         ft.commit();     }  } 

The activity_in_tab.xml is just this:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/content"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:isScrollContainer="true"> </RelativeLayout> 

As you can see, the view layout for each tab was the same. That's because it's just a FrameLayout called content that will hold each fragment. The fragments are the ones that have each screen's view.

Just for the bonus points, we also added some little code to show a confirm dialog when the user presses Back and there are no more fragments to go back to:

// In ActivityInTab.java... @Override public void onBackPressed() {     FragmentManager manager = getSupportFragmentManager();     if (manager.getBackStackEntryCount() > 0) {         // If there are back-stack entries, leave the FragmentActivity         // implementation take care of them.         super.onBackPressed();     } else {         // Otherwise, ask user if he wants to leave :)         showExitDialog();     } } 

That's pretty much the setup. As you can see, each FragmentActivity (or just simply Activity in Android >3) is taking care of all the back-stacking with it's own FragmentManager.

An activity like ActivityInTab1 will be really simple, it'll just show it's first fragment (i.e. screen):

public class ActivityInTab1 extends ActivityInTab {     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         navigateTo(new Fragment1());     } } 

Then, if a fragment needs to navigate to another fragment, it has to do a little nasty casting... but it's not that bad:

// In Fragment1.java for example... // Need to navigate to Fragment2. ((ActivityIntab) getActivity()).navigateTo(new Fragment2()); 

So that's pretty much it. I'm pretty sure this is not a very canonical (and mostly sure not very good) solution, so I'd like to ask seasoned Android developers what would be a better approach to acheive this functionality, and if this is not "how it's done" in Android, I'd appreciate if you could point me to some link or material that explains which is the Android way to approach this (tabs, nested screens in tabs, etc). Feel free to tear apart this answer in the comments :)

As a sign that this solution is not very good is that recently I had to add some navigation functionality to the application. Some bizarre button that should take the user from one tab into another and into a nested screen. Doing that programmatically was a pain in the butt, because of who-knows-who problems and dealing with when are fragments and activities actually instantiated and initialized. I think it would have been much easier if those screens and tabs were all just Views really.


Finally, if you need to survive orientation changes, it's important that your fragments are created using setArguments/getArguments. If you set instance variables in your fragments' constructors you'll be screwed. But fortunately that's really easy to fix: just save everything in setArguments in the constructor and then retrieve those things with getArguments in onCreate to use them.

like image 42
epidemian Avatar answered Sep 20 '22 00:09

epidemian