Android: Multiple Fragments stack in each ViewPager Tab


We will try to accomplish the following scenario:

MainActivity
    |
    |
ContainerFragment
    |
    |
    |_ _ _ Tab A
    |        |_ _ _ Fragment 1
    |        |
    |        |_ _ _ Fragment 2
    |        |
    |        |_ _ _ Fragment 3
    |        |
    |        |_ _ _ ...
    |        
    |_ _ _ Tab B
    |        |_ _ _ Fragment 4
    |        |
    |        |_ _ _ Fragment 5
    |        |
    |        |_ _ _ Fragment 6
    |        |
    |        |_ _ _ ...
    |
    |_ _ _ Tab C
    |        |_ _ _ Fragment 7
    |        |
    |        |_ _ _ Fragment 8
    |        |
    |        |_ _ _ Fragment 8
    |        |
    |        |_ _ _ ...

Check out the apk file and/or video to see the final result before going further:

https://raw.githubusercontent.com/tausiq/ViewPagerMultipleFragmentDemo/master/release/app_0.0.1.apk

There is one MainActivity and inside the MainActivity there is one Fragment, which is a container of a ViewPager with 3 tabs.

We would like to maintain multiple Fragment stack or multiple child fragments inside each tab.
This can be easily accomplished with the help of ChildFragmentManager.

layout-2014-06-06-124959

device-2014-06-06-125113

device-2014-06-06-125125

Lets start with the MainActivity class. We have only one Activity class for simplicity and demo purpose.

MainActivity.java


import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;


public class MainActivity extends ActionBarActivity {

    private CarouselFragment carouselFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setHomeButtonEnabled(true);

        if (savedInstanceState == null) {

            // withholding the previously created fragment from being created again
            // On orientation change, it will prevent fragment recreation
            // its necessary to reserve the fragment stack inside each tab
            initScreen();

        } else {
            // restoring the previously created fragment
            // and getting the reference
            carouselFragment = (CarouselFragment) getSupportFragmentManager().getFragments().get(0);
        }
    }

    private void initScreen() {
        // Creating the ViewPager container fragment once
        carouselFragment = new CarouselFragment();

        final FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.container, carouselFragment)
                .commit();
    }

    /**
     * Only Activity has this special callback method
     * Fragment doesn't have any onBackPressed callback
     *
     * Logic:
     * Each time when the back button is pressed, this Activity will propagate the call to the
     * container Fragment and that Fragment will propagate the call to its each tab Fragment
     * those Fragments will propagate this method call to their child Fragments and
     * eventually all the propagated calls will get back to this initial method
     *
     * If the container Fragment or any of its Tab Fragments and/or Tab child Fragments couldn't
     * handle the onBackPressed propagated call then this Activity will handle the callback itself
     */
    @Override
    public void onBackPressed() {

        if (!carouselFragment.onBackPressed()) {
            // container Fragment or its associates couldn't handle the back pressed task
            // delegating the task to super class
            super.onBackPressed();

        } else {
            // carousel handled the back pressed task
            // do not call super
        }
    }
}

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

CarouselFragment.java


import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.viewpagerindicator.TabPageIndicator;


/**
 * A simple {@link Fragment} subclass.
 *
 */
public class CarouselFragment extends Fragment {

    /**
     * TabPagerIndicator
     *
     * Please refer to ViewPagerIndicator library
     */
    protected TabPageIndicator indicator;

    protected ViewPager pager;

    private ViewPagerAdapter adapter;


    public CarouselFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.fragment_carousel, container, false);

        indicator = (TabPageIndicator) rootView.findViewById(R.id.tpi_header);
        pager = (ViewPager) rootView.findViewById(R.id.vp_pages);

        return rootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Note that we are passing childFragmentManager, not FragmentManager
        adapter = new ViewPagerAdapter(getResources(), getChildFragmentManager());

        pager.setAdapter(adapter);
        indicator.setViewPager(pager);
    }

    /**
     * Retrieve the currently visible Tab Fragment and propagate the onBackPressed callback
     *
     * @return true = if this fragment and/or one of its associates Fragment can handle the backPress
     */
    public boolean onBackPressed() {
        // currently visible tab Fragment
        OnBackPressListener currentFragment = (OnBackPressListener) adapter.getRegisteredFragment(pager.getCurrentItem());

        if (currentFragment != null) {
            // lets see if the currentFragment or any of its childFragment can handle onBackPressed
            return currentFragment.onBackPressed();
        }

        // this Fragment couldn't handle the onBackPressed call
        return false;
    }

}

fragment_carousel.xml


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.viewpagerindicator.TabPageIndicator
        android:id="@+id/tpi_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_pages"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

ViewPagerAdapter.java


import android.content.res.Resources;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.util.SparseArray;
import android.view.ViewGroup;

import ms.cloudtea.viewpagermultiplefragmentdemo.app.frags.A1Fragment;
import ms.cloudtea.viewpagermultiplefragmentdemo.app.frags.B1Fragment;
import ms.cloudtea.viewpagermultiplefragmentdemo.app.frags.C1Fragment;

/**
 * Created by shahabuddin on 6/6/14.
 */
public class ViewPagerAdapter extends FragmentPagerAdapter {

    private final Resources resources;

    SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();

    /**
     * Create pager adapter
     *
     * @param resources
     * @param fm
     */
    public ViewPagerAdapter(final Resources resources, FragmentManager fm) {
        super(fm);
        this.resources = resources;
    }

    @Override
    public Fragment getItem(int position) {
        final Fragment result;
        switch (position) {
            case 0:
                // First Fragment of First Tab
                result = new A1Fragment();
                break;
            case 1:
                // First Fragment of Second Tab
                result = new B1Fragment();
                break;
            case 2:
                // First Fragment of Third Tab
                result = new C1Fragment();
                break;
            default:
                result = null;
                break;
        }

        return result;
    }

    @Override
    public int getCount() {
        return 3;
    }

    @Override
    public CharSequence getPageTitle(final int position) {
        switch (position) {
            case 0:
                return resources.getString(R.string.page_1);
            case 1:
                return resources.getString(R.string.page_2);
            case 2:
                return resources.getString(R.string.page_3);
            default:
                return null;
        }
    }

    /**
     * On each Fragment instantiation we are saving the reference of that Fragment in a Map
     * It will help us to retrieve the Fragment by position
     *
     * @param container
     * @param position
     * @return
     */
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        registeredFragments.put(position, fragment);
        return fragment;
    }

    /**
     * Remove the saved reference from our Map on the Fragment destroy
     *
     * @param container
     * @param position
     * @param object
     */
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        registeredFragments.remove(position);
        super.destroyItem(container, position, object);
    }


    /**
     * Get the Fragment by position
     *
     * @param position tab position of the fragment
     * @return
     */
    public Fragment getRegisteredFragment(int position) {
        return registeredFragments.get(position);
    }
}

Each Tab Fragment and the child Fragments of each Tab Fragment need to implement an Interface OnBackPressListener

OnBackPressListener.java


public interface OnBackPressListener {

    public boolean onBackPressed();
}

Common implementation of OnBackPressListener interface. This implementation should be used by all Tab Fragments and their child Fragments.

BackPressImpl.java


import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;

/**
 * Created by shahabuddin on 6/6/14.
 */
public class BackPressImpl implements OnBackPressListener {

    private Fragment parentFragment;

    public BackPressImpl(Fragment parentFragment) {
        this.parentFragment = parentFragment;
    }

    @Override
    public boolean onBackPressed() {

        if (parentFragment == null) return false;

        int childCount = parentFragment.getChildFragmentManager().getBackStackEntryCount();

        if (childCount == 0) {
            // it has no child Fragment
            // can not handle the onBackPressed task by itself 
            return false;
            
        } else {
            // get the child Fragment 
            FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
            OnBackPressListener childFragment = (OnBackPressListener) childFragmentManager.getFragments().get(0);

            // propagate onBackPressed method call to the child Fragment  
            if (!childFragment.onBackPressed()) {
                // child Fragment was unable to handle the task 
                // It could happen when the child Fragment is last last leaf of a chain 
                // removing the child Fragment from stack 
                childFragmentManager.popBackStackImmediate();

            }
            
            // either this Fragment or its child handled the task 
            // either way we are successful and done here 
            return true;
        }
    }
}

All the Tab Fragments and child Fragments of each Tab should be a sub class of a common Fragment class. This super class will handle the common onBackPressed callback

RootFragment.java


import android.support.v4.app.Fragment;

import ms.cloudtea.viewpagermultiplefragmentdemo.app.BackPressImpl;
import ms.cloudtea.viewpagermultiplefragmentdemo.app.OnBackPressListener;

/**
 * Created by shahabuddin on 6/6/14.
 */
public class RootFragment extends Fragment implements OnBackPressListener {

    @Override
    public boolean onBackPressed() {
        return new BackPressImpl(this).onBackPressed();
    }
}

You can get the full source code from here:

https://github.com/tausiq/ViewPagerMultipleFragmentDemo

24 thoughts on “Android: Multiple Fragments stack in each ViewPager Tab

  1. I see in you code a lot of empty constructors in Fragments with comment “Required empty public constructor”. I belive they all useless becouse Java automatically create empty public constructor if there no any other constructors. This is requared only if there some constructor with params.

  2. ClasscastException at:

    OnBackPressListener currentFragment = (OnBackPressListener) mSectionsPagerAdapter.getRegisteredFragment(mViewPager.getCurrentItem());

  3. Notice that new fragments are not replacing previous fragments.
    It seems like they are being replaced because of android:background=”@android:color/white” in added fragments.

  4. Is there a way to determine when app is about to throw a StackOverFlow for creating too many Fragments (and possible do something about it) ?

  5. Hi.
    I’m trying to implement your source code but I’m getting errors with the call to getFragments() and getChildFragmentManager() methods. They seem to not be recognized as part of Fragment() method, and I suppose is due to some kind of conflict between libraries v4 and v7.
    So the question is: how should I add android-support-v4 and android-support-v7?
    Thanks.

  6. As Gero mentioned… new fragments appear in front of parent fragments, but parent fragments are active as well as their listeners.

    I tried to hide parent fragment with beginTransaction().hide().commit(); but this also hides the new child fragment. The only hack I could come up with was to set a big click listener for my new child fragment’ layout.

  7. In the tutorial, it’s only for if you have one activity with one fragment container. I have managed to implement this successfully. But how do you go about it if you have one activity with multiple fragment container that hold view pager. In other words, in your app you have more than one tabbed layout.

  8. @Gero comment is the same problem i am having. Fraagmnets are not getting replaced but they are overlapping. Does anyone know what is the problem here?

  9. Hi everyone! Who can tell me why FrameLayout with @id fragment_mainLayout is container??? tks you ❤

  10. This was really helpful and this is the exact thing i was searching for days.i have one question on how to show the tabs in the bottom of the screen?

  11. Could anyone tell please tell me how to manually apply backpress using above code?

    I am calling onBackPressed() method on a button click but nothing is happening.

    Thanks in advance!

  12. Can anyone help me, regarding the fragment overlapping issue, using this example.
    Due to Fragment overlapping, the controls from back fragment are accessible in the top fragment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s