Separate Back Stack for each tab in Android using Fragments
Asked Answered
B

12

161

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

Bedplate answered 8/8, 2011 at 19:14 Comment(0)
O
23

The framework won't currently do this for you automatically. You will need to build and manage your own back stacks for each tab.

To be honest, this seems like a really questionable thing to do. I can't imagine it resulting in a decent UI -- if the back key is going to do different things depending on the tab I am, especially if the back key also has its normal behavior of closing the entire activity when at the top of the stack... sounds nasty.

If you are trying to build something like a web browser UI, to get a UX that is natural to the user is going to involve a lot of subtle tweaks of behavior depending on context, so you'll definitely need to do your own back stack management rather than rely on some default implementation in the framework. For an example try paying attention to how the back key interacts with the standard browser in the various ways you can go in and out of it. (Each "window" in the browser is essentially a tab.)

Osteoclasis answered 17/8, 2011 at 7:18 Comment(20)
Wouldn't it be better to use FragmentActivity for each tab? Since fragment framework is so useless then it comes to non-trivial things.Cad
Don't do that. And the framework is hardly useless. It doesn't give you automatic support for this kind of thing, which as I said I can't imagine resulting in a decent user experience except in very specialized situations where you are will need to carefully control the back behavior anyway.Osteoclasis
This type of navigation, then you have tabs and hierarchy of pages on each tab is very common for iPhone applications for example (you can check App Store and iPod apps). I find their user experience quite decent.Cad
This is insane. The iPhone doesn't even have a back button. There are API demos showing very simple code to implement fragments in tabs. The question being asked was about having different back stacks for each tab, and my response is that the framework doen't supply this automatically because semantically for what the back button does it would most likely be a crappy user experience. You can fairly easily implement back semantics yourself if you want though.Osteoclasis
You think iPhone-like navigation withing a tab using back key is a bad idea? Never thought about this. This is arguable I think. Anyway, implementing nice backstack behavior is quite a work. I think it is better to just stick with activities and save myself a lot of time.Cad
Again, the iPhone doesn't have a back button, so it doesn't semantically have back stack behavior like Android. Also "better to just stick with activities and save myself a lot of time" doesn't make any sense here, because activities don't let you put maintain tabs in a UI with their own different back stacks; in fact back stack management of activities is less flexible than what is provided by the Fragment framework.Osteoclasis
It doesn't have hard back button, but it adds soft "back" button to the top left corner of a page by default. And manages behavior of this button. Just like Android fragments backstack, but with full tabs support. Now I have ActivityGroup with tabs, and one FragmentActivity per tab. So I have separate backstack of Fragments per tab and everything working just perfect atm.Cad
@Osteoclasis I am trying to follow your points, but have been having trouble implementing custom back-stack behaviour. I realise that having been involved in the design of this, you would have a solid insight into how easy it might be. Is it possible that there is a demo app for the OP's use case, as it really is a very common situation, especially for those of us who have to write & port iOS apps for clients who make these requests.... management of separate fragment backstacks within each FragmentActivity.Danelaw
How one would go at implementing custom back stacks ? Unfortunately all solutions that handle this resort to ActivityGroup. What is the big problem in ActivityGroup anyway ? Everyone say to keep away from it like the plague. If the new API can't do what the deprecated could, then I guess its not time for depreciation ?Excusatory
@nobre ActivityGroup can be use but leads to horrible memory management. You feel the jitters and if you are using it to build a really heavy app that shows Graphs and all (I did) it will give you OutOfStack and OutOfMemory exceptions. I am looking for the back stack management experiment as well. Any help from others would b great.Karim
@boulder I know TabActivity has been deprecated. So the method of maintaining separate backstack sounds only valid option.. But where I can find information about creating and maintaining separate back stack for fragments? No one seemed to ask that questionWhimsicality
@Whimsicality it's depricated so what? It was declared depricated just because it isn't recomended way to do tabs. But if recomended way suks it's resonable to use not-recomended one. TabActivity is very simple, it is unlikely it will break because of lack of support from google. And even if it will it is cost about no effort to implement you own tab activity.Cad
@boulder I already used TabActivity and ActivityGroup in my 2 of my applications even though they were deprecated.. Now I started learning fragments and want to do fragments with tabs. BTW can you provide any link which discuss about creating and maintaining back stack?Whimsicality
@Whimsicality no, I don't use fragments at all.Cad
Well I got it working at last, I created and maintained my own navigation stack for each tabs, and I used fragments, not activities.. Now I feel this approach is better than the one with activities. You have more control and you don't have to use any deprecated class (whatever deprecated means). Thanks all for the input.. This has been a very informative threadWhimsicality
@Whimsicality Are you willing to post some sample code somewhere? This would be quite useful.Linares
This question is about a UX pattern that is very common in iOS apps. Many of us are expected to port iOS apps, where our client prefers more or less similar navigation. And frankly, I can't find a good reason to contradict my client and use an entirely different navigation behavior on Android. My usability tests confirm that (if well done) android users totally get it. So who are you to claim this is bad UX, without having seen the app?Galengalena
Well, based on this tiny comment in API docs, I guess Google considered a possibility of somebody wanting to do this exact behavior, see: developer.android.com/reference/android/app/… -> Some applications may use this action to return to the top level of a category.Balfour
Nested fragments + Pager + back stack + config changes = real fun!Adder
Yeah exactly. I laugh @Osteoclasis trying to pretend the issue isn't with how bad Android is written. Every piece of the Android framework is a fragile flower that can only be used the way the founding Android fathers (and mothers) intended. And even then, cross your fingers.Joelie
W
140

Read this before using this solution

Wow, I still can't believe this answer is the one with most votes in this thread. Please don't blindly follow this implementation. I wrote this solution in 2012 (when I was just a novice in Android). Ten years down the line, I can see there is a terrible issue with this solution.

I am storing hard reference to fragments to implement the navigation stack. It is a terrible practice and would result in memory leak. Let the FragmentManager saves the reference to fragments. Just store the fragment identifier if needed.

My answer can be used with above modification if needed. But I don't think we need to write a multi stacked navigation implementation from scratch. There is surely a much better readymade solution for this. I am not much into Android nowadays, so can't point to any.

I am keeping the original answer for the sake of completeness.

Original answer

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..

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

Whimsicality answered 17/10, 2012 at 6:36 Comment(39)
Have you thought of how to manage orientation changes? It's not good practice to save views (fragments) in the savedInstanceState bundle, but I can't see any other option...Caco
@omegatai never had to handle orientation change in any of our projects until now. But my approach would be 1)save all my data in activity 2)do non UI logic in activity 3)Fragment every time create from existing data present in activity. I would never save entire view in savedInstanceState, that would be suicide.Whimsicality
One other concern is when user is inputing text to EditText, and rotation happens without saving that text to data structure. I would save those texts in savedInstanceState and display then in target orientation, to avoid user reentering them again.. Well, I may have to face more challenges, but will have to tackle it as it comesWhimsicality
Storing data for every fragment, recreating every one of them, rebuilding the stacks... so much work for a simple orientation change.Caco
@omegatai completely agree with you.. All the problem arises since Android doesn't manage stack for us (which iOS does and orientation change or tab with multiple fragment is a breeze) and which takes back us to the original discussion in this Q/A thread. No good going back to that now..Whimsicality
You somehow need to split the activity in to smaller classes/files if you have multiple tab structure.. Else activity code will be huge..Whimsicality
Google considers fragment as an entity for hot swapping. You create when you need, destroy when you don't need. So it should be optimized for quick recreation or destruction. But it is very difficult to manage (if you have many).Whimsicality
Thanks for the wonderful solution! I have a question. Each of the fragment in a tab should fetch data from server on initial load. It seems to work once. But if I switch tabs and come back again, instead of showing pre-loaded view, it connects to server again. How can I retain the view already loaded in each tab?Uniliteral
@Uniliteral This is because fragment is recreated every time, when you switch tab.. Dont think for even once that, your fragment is reused across tab switch. when I switch from A tab to B, A tab is freed from memory. So save your data in activity, and every time check whether activity has data before trying to get it from server.Whimsicality
@Whimsicality Thanks Krishnabhadra! Yeah I saw that you're replacing fragment each time you switch the tab. I wrap server data into a custom adapter. What do you think is the best way to save data? in bundle or somewhere?Uniliteral
That depends on the data. If data is heavy, why dont you create an object inside activity, which can be modfied from any of the child fragments? Use bundle only if your data is smallWhimsicality
@Whimsicality Ok, that sounds much better. Let me correct in case I'm wrong. As per your example, there is only one activity and therefore one bundle. Create adapter instances in the BaseFragment(refering your project) and save data in there. Use them whenever view is to be built.Uniliteral
great work , i have a question,, if i want to go back onBack Press to Fragemnt that is not added in tab bar , how i manage to go back ?Indemnification
@Indemnification If you have a workflow like that, then it is better to forget using tabs.Whimsicality
its possible to override OnBackPress in that Specific non AppContant Fragmnet of base Class,thanksIndemnification
@Whimsicality i am using this code and having problem in onActviityResult ,i'm getting null pointerBattik
It also may be reasonable to track tab history like Instagram or Facebook apps do. Instead of finishing tab activity those apps switch to other non-empty backstacks (other tabs).Brahms
Got it to work. Thanks a lot. Uploading the whole project was a good idea! :-)Ellsworth
@Krishnabhadra: hey, thanks for your great soltion, please read my answer below, I have one issue...Meilhac
@Whimsicality nice demo , Can we stop to everytime calling OnCreateView in Fragement? Actually when Fragment A -> Fragement B its fine call OnCreateView ,but when we popFragments Fragmet B -> Fragement A it also called OnCreateView so every time it create new view. I want to stop this like Activity.Can we do this?Thomism
@Whimsicality : I have exactly same problem. I have 5 tabs in my activity. In tab A, from FRAGMENT A -> Fragment A1 , and when I press back button from Fragment A1, the whole view is again loading in Fragment A. I have listview in fragment A, and listview data is coming from server. So its heavy problem that every time data is loading from server on back press from A1 to A, and also listview scroll is never maintained.. You found any solution for this? or any one else?Chinoiserie
@MihirShah I think my comment (third comment below this answer now) to user omegatai's comment answers your question. BTW if you have a new question regarding this, post it as a new question, and link to this one if you want to include this post.Whimsicality
I donwloaded the ZIP from git and when I run the uploaded sample it crashes. Does anyone had the same problem? What is the fix?Alesha
@Lunatikul hmm I couldn't get time to go back and update the project. Something might have broken with new API changes. Will update the project when I get time.Whimsicality
@Whimsicality i have modified a bit your code, see an answer below. if you want, you can update your code to be able to work with configuration changes (rotations mainly)Cudlip
@Cudlip Actually I have done it already. You have lot of freedom with this code actually. But I didn't know that when I uploaded it to github.Whimsicality
@Krishnabhadra: please help me with this: stackoverflow.com/questions/19581034Cudbear
But when ever I want to re-add any fragment that I already created and stored in its custom stack(as an object), all it's life cycle methods are called once again as are called first time. This is the problem. In this case all the views of fragment layout are recreated and it behaves like a new fragment. So how to avoid this ?Ivey
@RahulBhardwaj have you read all comments. 3rd comment below this post is the way to go. You should look at fragment as an entity for hot swapping. So save all your data in activity, and just unplug and replug fragment as you need.Whimsicality
@Whimsicality : I am using ActionBar and am facing the same problem: #19604034 . Can you please provide some insight / inputs?Mcgrew
@Krishnabhadra: when I change from using TabHost to FragmentTabHost, onCreateView of Fragment is fired twice, can you suggest anything for me? I think it has something to do with adding tab, because I have to change to add tab like this: mTabHost.addTab(spec, AppTabAFirstFragment.class, null);Cudbear
@Krishnabhadra: many thanks for sharing your work, I had no time to solve this problem myself and you saved me. I still haven't been able to find a satisfactory solution for persisting the fragment view on tab changes (i.e. a network request should not be performed if data has already been downloaded), but I'm working on it and hopefully will be able to share my solution soon ... :)Inbreed
@Krishnabhadra: please read the issue I posted below and give me replyMeilhac
Where is the Donate button !!! It was exceptionally good explanation and every thing is working as expected.Neigh
Thanks for the code.. It helps me lot.. Now I have one doubt. I have 7 fragments in Tab A. Now I want to navigate to the home fragment and clear all the back stack fragments. Please let me know how to clear all the fragments. Now I am trying like this: public void ClearAllPages(){ int pageCount=mStacks.get(mCurrentTab).size(); for(int i=1;i<pageCount;i++){ popFragments(); } } But When I call this from the last fragment I am getting No host exception. Please give me any idea to clear all the pages and go back to home fragment.Kiruna
@Kiruna You don't need to call popFragment(). Just clear the stack for that tab like mStacks.get(mCurrentTab).clear(). Then push your home fragment. All non visible fragments (backstack) is already poped, with their only reference resides in the stack we manage. You just need to remove it from stack.Whimsicality
Hi @Krishnabhadra.. Is it possible to set the tab color programmatically? For example I want to navigate from tab B fragment 2, to the tab A. If i click the button in the fragment 2(Tab B) I want to go to Tab A and I want to change the Tab A selected color? Please give me any idea for this problem. Thanks in advance.Kiruna
How it's been work with android.support.design.widget.BottomNavigationViewIllboding
Kindly use this link to create a back stack as used in Instagram medium.com/@smihajlovskih/…Classify
A
98

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.

Asgard answered 20/9, 2011 at 4:16 Comment(20)
Great answer but I think very few will see this. I choosed exactly the same path (as you can see from conversation in previous answer) and I'm not happy with it just like you. I think Google really screwed up with this fragments since this API doesn't cover major use cases. Another issue you may run into is impossibility to embed fragment into another fragment.Cad
Thanks for the comment boulder. Yeah, I couldn't agree more about the fragments API. I have already run into the problem of nested fragments (that's why we went for the "replace one fragment with another one" approach hehe).Asgard
I have implemented this via ALL activities. I didn't like what I got and I'm going to try Fragments. That's the opposite of your experience! There's a lot of implementation with Activities to handle the lifecycle of child views in each tab and also to implement your own back button. Also, you can't just keep a reference to all views or you will blow up memory. I hope Fragments will: 1) Support the lifecycle of the Fragments with clear separation of memory and 2) help implement the back button functionality Plus, if you use fragments for this process won't it be easier to run on Tablets?Laquanda
What happens when the user switches tabs? Does the Fragment backstack get deleted? How to make sure the backstack remains?Laquanda
@Laquanda If you go 1 tab <-> 1 activity like I did, the backstack for each tab will remain when tabs are switched because the activities are actually kept alive; they are only paused and resumed. I don't know if there is a way to make the activities be destroyed and re-created when tabs are switched in a TabActivity. However, if you make the fragments inside the activities be replaced like I suggested, they are destroyed (and re-created when the backstack is popped). So you will have at most one fragment alive per tab at any time.Asgard
@epidermian Thanks for the answer. I'm working on an android app. I use a similar approach as yours. Another problem I'm working on is that fragments transition animation is far from activities. This has been mentioned here. Does anybody have a solution?Hideaway
This is a pretty good solutions. Haven't worked on this extensively to know what breaks it. But this seems to be doing the same thing as activity group did with activities.Shook
@epidemian:- Thanks for the answer easily solved my problem related to Tabs & fragments.Fine one +1 for this.Perrine
@Asgard Most of the developers are switching to fragments based solution for tabbed android application because TabActivity, ActivityGroup and LocalActivityGroup are deprecated. The solution which has been given by you uses TabActivity(which is deprecated). Have you managed to get this solution working without using the TabActivity or any other deprecated class?Turaco
@AshishPathak I haven't tried. We used this approach for an Android 2.x application, so it wasn't "bad" to use TabActivity back then. Technically, we didn't need fragments, but we used them because of their easy "stackability".Asgard
@Asgard I trying to use what you write above, but i am stuck at rotations. As id my tab 1 have fragment A and B. First open A the push at B, then rotate my device and i go back to B. How i could this improve maybe someone know ?Contravention
@Contravention sorry, but i think i can't help you :(. It's been a while since i did anything in Android. I do remember that orientation changes could be a pain in the butt, because you had to be sure all the state in your activities was properly stored/restored (because the activities were re-created on each orientation change as far as i remember)... but i don't really know how that mixes up with this fragment-thing solution. When we programmed it we didn't take orientation changes into consideration, and had the orientation blocked in portrait position for all the app.Asgard
@Contravention did you end up figuring it out? It sounds like rotating the screen is somehow causing your Activity to push another copy of fragment B onto the backstack. Have you looked into this at all since? You could also override onConfigurationChanged to fix this behavior, but usually you want to use this as a last resort. It sounds like your Activity is incorrectly adding a fragment to the backstack each time the Activity is resumed and/or created.Prohibitive
No i didn't tried yet. Don't really had the time but eventually i should this figure out. Thanks for the tip.Contravention
@Asgard i know it's been a while , but i have some questions regarding that solution, it really helped me.Milewski
@Reham, yep, it's been a while :). If the questions are adequate to be comments on this answer, go ahead and ask them (i can't promise i'll remember too much about this code really hehe). Maybe, if it's general enough to help other people, you could ask it as a real StackOverflow question :)Asgard
Ok thank you..:) I used your code and i built a custom action bar with menu button, when i click on any item i just want to show the fragment related to that item in the ActivityIntab like the others.but i did not get a way to do that..i will try to ask the other people thank you.Milewski
@Asgard If we add/replace new fragments in child fragment container in each tab fragment. We can use .getChildFragmentManager for the transaction within a tab. if child fragment back stack is empty call tab fragment back press event. What are your thoughts?Cockalorum
@Cockalorum sorry, i've no idea. I haven't kept up with Android development :/Asgard
@Asgard ok no problems. Thanks for the answer.Cockalorum
O
23

The framework won't currently do this for you automatically. You will need to build and manage your own back stacks for each tab.

To be honest, this seems like a really questionable thing to do. I can't imagine it resulting in a decent UI -- if the back key is going to do different things depending on the tab I am, especially if the back key also has its normal behavior of closing the entire activity when at the top of the stack... sounds nasty.

If you are trying to build something like a web browser UI, to get a UX that is natural to the user is going to involve a lot of subtle tweaks of behavior depending on context, so you'll definitely need to do your own back stack management rather than rely on some default implementation in the framework. For an example try paying attention to how the back key interacts with the standard browser in the various ways you can go in and out of it. (Each "window" in the browser is essentially a tab.)

Osteoclasis answered 17/8, 2011 at 7:18 Comment(20)
Wouldn't it be better to use FragmentActivity for each tab? Since fragment framework is so useless then it comes to non-trivial things.Cad
Don't do that. And the framework is hardly useless. It doesn't give you automatic support for this kind of thing, which as I said I can't imagine resulting in a decent user experience except in very specialized situations where you are will need to carefully control the back behavior anyway.Osteoclasis
This type of navigation, then you have tabs and hierarchy of pages on each tab is very common for iPhone applications for example (you can check App Store and iPod apps). I find their user experience quite decent.Cad
This is insane. The iPhone doesn't even have a back button. There are API demos showing very simple code to implement fragments in tabs. The question being asked was about having different back stacks for each tab, and my response is that the framework doen't supply this automatically because semantically for what the back button does it would most likely be a crappy user experience. You can fairly easily implement back semantics yourself if you want though.Osteoclasis
You think iPhone-like navigation withing a tab using back key is a bad idea? Never thought about this. This is arguable I think. Anyway, implementing nice backstack behavior is quite a work. I think it is better to just stick with activities and save myself a lot of time.Cad
Again, the iPhone doesn't have a back button, so it doesn't semantically have back stack behavior like Android. Also "better to just stick with activities and save myself a lot of time" doesn't make any sense here, because activities don't let you put maintain tabs in a UI with their own different back stacks; in fact back stack management of activities is less flexible than what is provided by the Fragment framework.Osteoclasis
It doesn't have hard back button, but it adds soft "back" button to the top left corner of a page by default. And manages behavior of this button. Just like Android fragments backstack, but with full tabs support. Now I have ActivityGroup with tabs, and one FragmentActivity per tab. So I have separate backstack of Fragments per tab and everything working just perfect atm.Cad
@Osteoclasis I am trying to follow your points, but have been having trouble implementing custom back-stack behaviour. I realise that having been involved in the design of this, you would have a solid insight into how easy it might be. Is it possible that there is a demo app for the OP's use case, as it really is a very common situation, especially for those of us who have to write & port iOS apps for clients who make these requests.... management of separate fragment backstacks within each FragmentActivity.Danelaw
How one would go at implementing custom back stacks ? Unfortunately all solutions that handle this resort to ActivityGroup. What is the big problem in ActivityGroup anyway ? Everyone say to keep away from it like the plague. If the new API can't do what the deprecated could, then I guess its not time for depreciation ?Excusatory
@nobre ActivityGroup can be use but leads to horrible memory management. You feel the jitters and if you are using it to build a really heavy app that shows Graphs and all (I did) it will give you OutOfStack and OutOfMemory exceptions. I am looking for the back stack management experiment as well. Any help from others would b great.Karim
@boulder I know TabActivity has been deprecated. So the method of maintaining separate backstack sounds only valid option.. But where I can find information about creating and maintaining separate back stack for fragments? No one seemed to ask that questionWhimsicality
@Whimsicality it's depricated so what? It was declared depricated just because it isn't recomended way to do tabs. But if recomended way suks it's resonable to use not-recomended one. TabActivity is very simple, it is unlikely it will break because of lack of support from google. And even if it will it is cost about no effort to implement you own tab activity.Cad
@boulder I already used TabActivity and ActivityGroup in my 2 of my applications even though they were deprecated.. Now I started learning fragments and want to do fragments with tabs. BTW can you provide any link which discuss about creating and maintaining back stack?Whimsicality
@Whimsicality no, I don't use fragments at all.Cad
Well I got it working at last, I created and maintained my own navigation stack for each tabs, and I used fragments, not activities.. Now I feel this approach is better than the one with activities. You have more control and you don't have to use any deprecated class (whatever deprecated means). Thanks all for the input.. This has been a very informative threadWhimsicality
@Whimsicality Are you willing to post some sample code somewhere? This would be quite useful.Linares
This question is about a UX pattern that is very common in iOS apps. Many of us are expected to port iOS apps, where our client prefers more or less similar navigation. And frankly, I can't find a good reason to contradict my client and use an entirely different navigation behavior on Android. My usability tests confirm that (if well done) android users totally get it. So who are you to claim this is bad UX, without having seen the app?Galengalena
Well, based on this tiny comment in API docs, I guess Google considered a possibility of somebody wanting to do this exact behavior, see: developer.android.com/reference/android/app/… -> Some applications may use this action to return to the top level of a category.Balfour
Nested fragments + Pager + back stack + config changes = real fun!Adder
Yeah exactly. I laugh @Osteoclasis trying to pretend the issue isn't with how bad Android is written. Every piece of the Android framework is a fragile flower that can only be used the way the founding Android fathers (and mothers) intended. And even then, cross your fingers.Joelie
R
6

Storing strong references to fragments is not the correct way.

FragmentManager provides putFragment(Bundle, String, Fragment) and saveFragmentInstanceState(Fragment).

Either one is enough to implement a backstack.


Using putFragment, instead of replacing a Fragment, you detach the old one and add the new one. This is what the framework does to a replace transaction that is added to the backstack. putFragment stores an index to the current list of active Fragments and those Fragments are saved by the framework during orientation changes.

The second way, using saveFragmentInstanceState, saves the whole fragment state to a Bundle allowing you to really remove it, rather than detaching. Using this approach makes the back stack easier to manipulate, as you can pop a Fragment whenever you want.


I used the second method for this usecase:

SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
               \                          /
                \------------------------/

I don't want the user to return to the Sign Up screen, from the third one, by pressing the back button. I also do flip animations between them (using onCreateAnimation), so hacky solutions won't work, atleast without the user clearly noticing something is not right.

This is a valid use case for a custom backstack, doing what the user expects...

private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";

private MyBackStack mBackStack;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    if (state == null) {
        mBackStack = new MyBackStack();

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.add(R.id.act_base_frg_container, new SignInFragment());
        tr.commit();
    } else {
        mBackStack = state.getParcelable(STATE_BACKSTACK);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(STATE_BACKSTACK, mBackStack);
}

private void showFragment(Fragment frg, boolean addOldToBackStack) {
    final FragmentManager fm = getSupportFragmentManager();
    final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);

    FragmentTransaction tr = fm.beginTransaction();
    tr.replace(R.id.act_base_frg_container, frg);
    // This is async, the fragment will only be removed after this returns
    tr.commit();

    if (addOldToBackStack) {
        mBackStack.push(fm, oldFrg);
    }
}

@Override
public void onBackPressed() {
    MyBackStackEntry entry;
    if ((entry = mBackStack.pop()) != null) {
        Fragment frg = entry.recreate(this);

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.replace(R.id.act_base_frg_container, frg);
        tr.commit();

        // Pop it now, like the framework implementation.
        fm.executePendingTransactions();
    } else {
        super.onBackPressed();
    }
}

public class MyBackStack implements Parcelable {

    private final List<MyBackStackEntry> mList;

    public MyBackStack() {
        mList = new ArrayList<MyBackStackEntry>(4);
    }

    public void push(FragmentManager fm, Fragment frg) {
        push(MyBackStackEntry.newEntry(fm, frg);
    }

    public void push(MyBackStackEntry entry) {
        if (entry == null) {
            throw new NullPointerException();
        }
        mList.add(entry);
    }

    public MyBackStackEntry pop() {
        int idx = mList.size() - 1;
        return (idx != -1) ? mList.remove(idx) : null;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        final int len = mList.size();
        dest.writeInt(len);
        for (int i = 0; i < len; i++) {
            // MyBackStackEntry's class is final, theres no
            // need to use writeParcelable
            mList.get(i).writeToParcel(dest, flags);
        }
    }

    protected MyBackStack(Parcel in) {
        int len = in.readInt();
        List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
        for (int i = 0; i < len; i++) {
            list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
        }
        mList = list;
    }

    public static final Parcelable.Creator<MyBackStack> CREATOR =
        new Parcelable.Creator<MyBackStack>() {

            @Override
            public MyBackStack createFromParcel(Parcel in) {
                return new MyBackStack(in);
            }

            @Override
            public MyBackStack[] newArray(int size) {
                return new MyBackStack[size];
            }
    };
}

public final class MyBackStackEntry implements Parcelable {

    public final String fname;
    public final Fragment.SavedState state;
    public final Bundle arguments;

    public MyBackStackEntry(String clazz, 
            Fragment.SavedState state,
            Bundle args) {
        this.fname = clazz;
        this.state = state;
        this.arguments = args;
    }

    public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
        final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
        final String name = frg.getClass().getName();
        final Bundle args = frg.getArguments();
        return new MyBackStackEntry(name, state, args);
    }

    public Fragment recreate(Context ctx) {
        Fragment frg = Fragment.instantiate(ctx, fname);
        frg.setInitialSavedState(state);
        frg.setArguments(arguments);
        return frg;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(fname);
        dest.writeBundle(arguments);

        if (state == null) {
            dest.writeInt(-1);
        } else if (state.getClass() == Fragment.SavedState.class) {
            dest.writeInt(0);
            state.writeToParcel(dest, flags);
        } else {
            dest.writeInt(1);
            dest.writeParcelable(state, flags);
        }
    }

    protected MyBackStackEntry(Parcel in) {
        final ClassLoader loader = getClass().getClassLoader();
        fname = in.readString();
        arguments = in.readBundle(loader);

        switch (in.readInt()) {
            case -1:
                state = null;
                break;
            case 0:
                state = Fragment.SavedState.CREATOR.createFromParcel(in);
                break;
            case 1:
                state = in.readParcelable(loader);
                break;
            default:
                throw new IllegalStateException();
        }
    }

    public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
        new Parcelable.Creator<MyBackStackEntry>() {

            @Override
            public MyBackStackEntry createFromParcel(Parcel in) {
                return new MyBackStackEntry(in);
            }

            @Override
            public MyBackStackEntry[] newArray(int size) {
                return new MyBackStackEntry[size];
            }
    };
}
Resign answered 19/4, 2014 at 12:23 Comment(0)
D
6

This can be easily achieved with ChildFragmentManager

Here is post about this with associated project. take a look,

http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/

Deloris answered 17/6, 2014 at 2:57 Comment(0)
B
2

Disclaimer:


I feel this is the best place to post a related solution I have worked on for a similar type of problem that seems to be pretty standard Android stuff. It's not going to solve the problem for everyone, but it may help some.


If the primary difference between your fragments is only the data backing them up (ie, not a lot of big layout differences), then you may not need to actually replace the fragment, but merely swap out the underlying data and refresh the view.

Here's a description of one possible example for this approach:

I have an app that uses ListViews. Each item in the list is a parent with some number of children. When you tap the item, a new list needs to open with those children, within the same ActionBar tab as the original list. These nested lists have a very similar layout (some conditional tweaks here and there perhaps), but the data is different.

This app has several layers of offspring beneath the initial parent list and we may or may not have data from the server by the time a user attempts to access any certain depth beyond the first. Because the list is constructed from a database cursor, and the fragments use a cursor loader and cursor adapter to populate the list view with list items, all that needs to happen when a click is registered is:

1) Create a new adapter with the appropriate 'to' and 'from' fields that will match new item views being added to the list and the columns returned by the new cursor.

2) Set this adapter as the new adapter for the ListView.

3) Build a new URI based on the item that was clicked and restart the cursor loader with the new URI (and projection). In this example, the URI is mapped to specific queries with the selection args passed down from the UI.

4) When the new data has been loaded from the URI, swap the cursor associated with the adapter to the new cursor, and the list will then refresh.

There is no backstack associated with this since we aren't using transactions, so you will have to either build your own, or play the queries in reverse when backing out of the hierarchy. When I tried this, the queries were fast enough that I just perform them again in oNBackPressed() up until I am at the top of hierarchy, at which point the framework takes over the back button again.

If you find yourself in a similar situation, make sure to read the docs: http://developer.android.com/guide/topics/ui/layout/listview.html

http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html

I hope this helps someone!

Bookcraft answered 11/8, 2012 at 19:39 Comment(1)
In case anyone is doing this, and also using a SectionIndexer (such as AlphabetIndexer), you may notice that after replacing the adapter, your fast scrolling doesn't work. Kind of an unfortunate bug, but replacing the adapter, even with a brand new indexer, doesn't update the list of sections used by FastScroll. There is a workaround, please see: description of problem and workaroundBookcraft
S
2

I had exactly the same problem and implemented an open source github project that covers stacked tab, back and up navigation and is well tested and documented:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

This is a simple and small framework for navigation tabs and fragment switching and handling of up and back navigation. Each tab has its own stack of fragments. It uses ActionBarSherlock and is compatible back to API level 8.

Schelling answered 27/3, 2013 at 18:7 Comment(0)
G
2

I'd like to suggest my own solution in case somebody is looking and want to try and choose the best one for his/her needs.

https://github.com/drusak/tabactivity

The purpose of creating the library is quite banal - implement it like iPhone.

The main advantages:

  • use android.support.design library with TabLayout;
  • each tab has its own stack using FragmentManager (without saving fragments' references);
  • support for deep linking (when you need to open specific tab and specific fragment's level in it);
  • saving / restoring states of tabs;
  • adaptive lifecycle methods of fragments in tabs;
  • quite easy to implement for your needs.
Gourmet answered 24/12, 2015 at 12:40 Comment(2)
Thank you, this has been quite helpful. I need to use ListFragments in addition to Fragments so I duplicated BaseTabFragment.java to BaseTabListFragment.java and had it extend ListFragment. Then I had to change various parts in the code where it always assumed to expect a BaseTabFragment. Is there a better way?Bayer
Unfortunately, Didn't think about ListFragment. Technically it is right solution, but it will require addition checks for TabFragment and its instanceOf BaseTabListFragment. Another approach to use Fragment with ListView inside (exactly the same as ListFragment implemeted). I'll think over it. Thanks for pointing Me on that!Gourmet
G
2

This is a complex problem as Android only handles 1 back stack, but this is feasible. It took me days to create a library called Tab Stacker that does exactly what you are looking for: a fragment history for each tab. It is open source and fully documented, and can be included easily with gradle. You can find the library on github: https://github.com/smart-fun/TabStacker

You can also download the sample app to see that the behaviour corresponds to your needs:

https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp

If you have any question don't hesitate to drop a mail.

Gilbertegilbertian answered 9/12, 2016 at 23:16 Comment(0)
V
0

A simple solution:

Every time you change tab/root view call:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

It will clear the BackStack. Remember to call this before you change the root fragment.

And add fragments with this:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();

Note the .addToBackStack(null) and the transaction.add could e.g. be changed with transaction.replace.

Vincentia answered 19/10, 2014 at 18:22 Comment(0)
C
-1

This thread was very very interesting and useful.
Thanks Krishnabhadra for your explanation and code, I use your code and improved a bit, allowing to persist the stacks, currentTab, etc... from change configuration (rotating mainly).
Tested on a real 4.0.4 and 2.3.6 devices, not tested on emulator

I change this part of code on "AppMainTabActivity.java", the rest stay the same. Maybe Krishnabhadra will add this on his code.

Recover data onCreate:

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
     */

  //if we are recreating this activity...
    if (savedInstanceState!=null) {
         mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
         mCurrentTab = savedInstanceState.getString("currentTab");
    }
    else {
    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.setup();

    initializeTabs();

  //set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
    mTabHost.setOnTabChangedListener(listener);
}

Save the variables and put to Bundle:

 //Save variables while recreating
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable("stack", mStacks);
    outState.putString("currentTab", mCurrentTab);
    //outState.putInt("tabHost",mTabHost);
}

If exist a previous CurrentTab, set this, else create a new Tab_A:

public void initializeTabs(){
    /* Setup your tab icons and content views.. Nothing special in this..*/
    TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);

    spec.setContent(new TabHost.TabContentFactory() {
        public View createTabContent(String tag) {
            return findViewById(R.id.realtabcontent);
        }
    });
    spec.setIndicator(createTabView(R.drawable.tab_a_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_b_state_btn));
    mTabHost.addTab(spec);

//if we have non default Tab as current, change it
    if (mCurrentTab!=null) {
        mTabHost.setCurrentTabByTag(mCurrentTab);
    } else {
        mCurrentTab=AppConstants.TAB_A;
        pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
    }
}

I hope this helps other people.

Cudlip answered 13/9, 2013 at 15:26 Comment(1)
This is wrong. When onCreate is called with a Bundle those Fragments won't be the same ones that are gonna be shown on screen and you're leaking the old ones, unless you're using setRetainInstance. And if the ActivityManager "saves" your Activity, since a Fragment is not Serializable nor Parcelable, when the user returns to your Activity, it will crash.Resign
S
-1

I would recommend do not use backstack based on HashMap> there is lots of bugs in "do not keep activities" mode. It will not correctly restore the state in case you deeply in fragment's stack. And also will be crached in nested map fragment (with exeption: Fragment no view found for ID) . Coz HashMap> after background\foreground app will be null

I optimize code above for work with fragment's backstack

It is bottom TabView

Main activity Class

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;

import com.strikersoft.nida.R;
import com.strikersoft.nida.abstractActivity.BaseActivity;
import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;

public class TagsActivity extends BaseActivity {
    public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
    private TabHost mTabHost;
    private String mCurrentTab;

    public static final String TAB_TAGS = "TAB_TAGS";
    public static final String TAB_MAP = "TAB_MAP";
    public static final String TAB_SETTINGS = "TAB_SETTINGS";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
        getActionBar().hide();
        setContentView(R.layout.tags_activity);

        mTabHost = (TabHost) findViewById(android.R.id.tabhost);

        mTabHost.setup();

        if (savedInstanceState != null) {
            mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
            initializeTabs();
            mTabHost.setCurrentTabByTag(mCurrentTab);
            /*
            when resume state it's important to set listener after initializeTabs
            */
            mTabHost.setOnTabChangedListener(listener);
        } else {
            mTabHost.setOnTabChangedListener(listener);
            initializeTabs();
        }
    }

    private View createTabView(final int id, final String text) {
        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));
        TextView textView = (TextView) view.findViewById(R.id.tab_text);
        textView.setText(text);
        return view;
    }

    /*
    create 3 tabs with name and image
    and add it to TabHost
     */
    public void initializeTabs() {

        TabHost.TabSpec spec;

        spec = mTabHost.newTabSpec(TAB_TAGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
        mTabHost.addTab(spec);

        spec = mTabHost.newTabSpec(TAB_MAP);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
        mTabHost.addTab(spec);


        spec = mTabHost.newTabSpec(TAB_SETTINGS);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
        mTabHost.addTab(spec);

    }

    /*
    first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
    for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
    */
    TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
        public void onTabChanged(String tabId) {

            mCurrentTab = tabId;

            if (tabId.equals(TAB_TAGS)) {
                pushFragments(SearchFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_MAP)) {
                pushFragments(MapContainerFragment.getInstance(), false,
                        false, null);
            } else if (tabId.equals(TAB_SETTINGS)) {
                pushFragments(SettingsFragment.getInstance(), false,
                        false, null);
            }

        }
    };

/*
Example of starting nested fragment from another fragment:

Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
                TagsActivity tAct = (TagsActivity)getActivity();
                tAct.pushFragments(newFragment, true, true, null);
 */
    public void pushFragments(Fragment fragment,
                              boolean shouldAnimate, boolean shouldAdd, String tag) {
        FragmentManager manager = getFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        if (shouldAnimate) {
            ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
                    R.animator.fragment_slide_left_exit,
                    R.animator.fragment_slide_right_enter,
                    R.animator.fragment_slide_right_exit);
        }
        ft.replace(R.id.realtabcontent, fragment, tag);

        if (shouldAdd) {
            /*
            here you can create named backstack for realize another logic.
            ft.addToBackStack("name of your backstack");
             */
            ft.addToBackStack(null);
        } else {
            /*
            and remove named backstack:
            manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
            or remove whole:
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
             */
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        ft.commit();
    }

    /*
    If you want to start this activity from another
     */
    public static void startUrself(Activity context) {
        Intent newActivity = new Intent(context, TagsActivity.class);
        newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(newActivity);
        context.finish();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putString(M_CURRENT_TAB, mCurrentTab);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onBackPressed(){
        super.onBackPressed();
    }
}

tags_activity.xml

<

?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="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_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:background="@drawable/bg_main_app_gradient"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
        <TabWidget
            android:id="@android:id/tabs"
            android:background="#EAE7E1"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>
    </LinearLayout>
</TabHost>

tags_icon.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tabsLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/bg_tab_gradient"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="contentDescription" >

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_marginTop="4dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView 
        android:id="@+id/tab_text"
        android:layout_marginBottom="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/tab_text_color"/>

</LinearLayout>

enter image description here

Scag answered 17/4, 2014 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.